--- /dev/null
+github: flaviotordini
+custom: https://flavio.tordini.org/donate
Translations are done at https://www.transifex.com/flaviotordini/minitube/
Just register and apply for a language team. Please don't request translation merges on GitHub.
-## Google API Key
-Google is now requiring an API key in order to access YouTube Data web services.
-Create a "Browser Key" at https://console.developers.google.com and enable the Youtube Data API.
-
-The key must be specified at compile time as shown below.
-Alternatively Minitube can read an API key from the GOOGLE_API_KEY environment variable.
-
## Build instructions
Clone from Github:
Compiling:
- qmake "DEFINES += APP_GOOGLE_API_KEY=YourAPIKeyHere"
+ qmake
make
Running:
#include "cachedhttp.h"
#include "localcache.h"
+#include <QtNetwork>
CachedHttpReply::CachedHttpReply(const QByteArray &body, const HttpRequest &req)
: bytes(body), req(req) {
deleteLater();
}
-WrappedHttpReply::WrappedHttpReply(LocalCache *cache, const QByteArray &key, HttpReply *httpReply)
- : HttpReply(httpReply), cache(cache), key(key), httpReply(httpReply) {
+WrappedHttpReply::WrappedHttpReply(CachedHttp &cachedHttp,
+ LocalCache *cache,
+ const QByteArray &key,
+ HttpReply *httpReply)
+ : HttpReply(httpReply), cachedHttp(cachedHttp), cache(cache), key(key), httpReply(httpReply) {
connect(httpReply, SIGNAL(data(QByteArray)), SIGNAL(data(QByteArray)));
connect(httpReply, SIGNAL(error(QString)), SIGNAL(error(QString)));
connect(httpReply, SIGNAL(finished(HttpReply)), SLOT(originFinished(HttpReply)));
void WrappedHttpReply::originFinished(const HttpReply &reply) {
qDebug() << reply.statusCode() << reply.url();
- if (reply.isSuccessful()) cache->insert(key, reply.body());
+ bool doCache = reply.isSuccessful();
+
+ if (doCache) {
+ const auto &validators = cachedHttp.getValidators();
+ if (!validators.isEmpty()) {
+ const QByteArray &mime = reply.header("Content-Type");
+ auto i = validators.constFind(mime);
+ if (i != validators.constEnd()) {
+ auto validator = i.value();
+ doCache = validator(reply);
+ } else {
+ i = validators.constFind("*");
+ if (i != validators.constEnd()) {
+ auto validator = i.value();
+ doCache = validator(reply);
+ }
+ }
+ }
+ }
+
+ if (doCache)
+ cache->insert(key, reply.body());
+ else
+ qDebug() << "Not caching" << reply.statusCode() << reply.url();
+
emit finished(reply);
}
return new CachedHttpReply(value, req);
}
qDebug() << "MISS" << key << req.url;
- return new WrappedHttpReply(cache, key, http.request(req));
+ return new WrappedHttpReply(*this, cache, key, http.request(req));
}
QByteArray CachedHttp::requestHash(const HttpRequest &req) {
void setMaxSize(uint maxSize);
void setCachePostRequests(bool value) { cachePostRequests = value; }
void setIgnoreHostname(bool value) { ignoreHostname = value; }
+ auto &getValidators() { return validators; };
HttpReply *request(const HttpRequest &req);
private:
LocalCache *cache;
bool cachePostRequests;
bool ignoreHostname = false;
+
+ /// Mapping is MIME -> validating function
+ /// Use * as MIME to define a catch-all validator
+ QMap<QByteArray, std::function<bool(const HttpReply &)>> validators;
};
class CachedHttpReply : public HttpReply {
Q_OBJECT
public:
- WrappedHttpReply(LocalCache *cache, const QByteArray &key, HttpReply *httpReply);
+ WrappedHttpReply(CachedHttp &cachedHttp,
+ LocalCache *cache,
+ const QByteArray &key,
+ HttpReply *httpReply);
QUrl url() const { return httpReply->url(); }
int statusCode() const { return httpReply->statusCode(); }
QByteArray body() const { return httpReply->body(); }
void originFinished(const HttpReply &reply);
private:
+ CachedHttp &cachedHttp;
LocalCache *cache;
QByteArray key;
HttpReply *httpReply;
CONFIG += c++17 exceptions_off rtti_off optimize_full object_parallel_to_source
TEMPLATE = app
-VERSION = 3.5
+VERSION = 3.5.1
DEFINES += APP_VERSION="$$VERSION"
APP_NAME = Minitube
include(src/qtsingleapplication/qtsingleapplication.pri)
include(src/invidious/invidious.pri)
+INCLUDEPATH += $$PWD/src
+
HEADERS += src/video.h \
src/messagebar.h \
src/spacer.h \
#include "cachedhttp.h"
#include "http.h"
#include "httputils.h"
+#include "jsfunctions.h"
#include "throttledhttp.h"
+#ifdef APP_EXTRA
+#include "extra.h"
+#endif
+
+namespace {
+QStringList fallbackServers{"https://invidious.snopyta.org"};
+QStringList preferredServers;
+
+void shuffle(QStringList &sl) {
+ std::shuffle(sl.begin(), sl.end(), *QRandomGenerator::global());
+}
+
+} // namespace
+
Invidious &Invidious::instance() {
static Invidious i;
return i;
Http &Invidious::cachedHttp() {
static Http *h = [] {
ThrottledHttp *throttledHttp = new ThrottledHttp(http());
- throttledHttp->setMilliseconds(300);
+ throttledHttp->setMilliseconds(500);
CachedHttp *cachedHttp = new CachedHttp(*throttledHttp, "iv");
cachedHttp->setMaxSeconds(86400);
cachedHttp->setIgnoreHostname(true);
+
+ cachedHttp->getValidators().insert("application/json", [](const auto &reply) -> bool {
+ const auto body = reply.body();
+ if (body.isEmpty() || body == "[]" || body == "{}") return false;
+ return true;
+ });
+
return cachedHttp;
}();
return *h;
}
-Invidious::Invidious(QObject *parent) : QObject(parent) {}
+Invidious::Invidious(QObject *parent) : QObject(parent) {
+#ifdef APP_EXTRA
+ preferredServers << Extra::extraFunctions()->stringArray("ivPreferred()");
+ shuffle(preferredServers);
+#endif
+ fallbackServers = JsFunctions::instance()->stringArray("ivFallback()");
+ shuffle(fallbackServers);
+}
void Invidious::initServers() {
- servers.clear();
- QUrl url("https://instances.invidio.us/instances.json?sort_by=type,health,users");
- auto reply = HttpUtils::yt().get(url);
+ if (!servers.isEmpty()) shuffleServers();
+
+ QString instanceApi = JsFunctions::instance()->string("ivInstances()");
+ if (instanceApi.isEmpty()) return;
+
+ auto reply = http().get(instanceApi);
connect(reply, &HttpReply::finished, this, [this](auto &reply) {
if (reply.isSuccessful()) {
+ servers.clear();
+
QSettings settings;
QStringList keywords = settings.value("recentKeywords").toStringList();
QString testKeyword = keywords.isEmpty() ? "test" : keywords.first();
- bool haveEnoughServers = false;
QJsonDocument doc = QJsonDocument::fromJson(reply.body());
for (const auto &v : doc.array()) {
+ if (servers.size() > 3) break;
+
auto serverArray = v.toArray();
QString host = serverArray.first().toString();
QJsonObject serverObj = serverArray.at(1).toObject();
if (serverObj["type"] == "https") {
QString url = "https://" + host;
- if (haveEnoughServers) break;
QUrl testUrl(url + "/api/v1/search?q=" + testKeyword);
auto reply = http().get(testUrl);
- connect(reply, &HttpReply::finished, this,
- [this, url, &haveEnoughServers](auto &reply) {
- if (!haveEnoughServers && reply.isSuccessful()) {
- QJsonDocument doc = QJsonDocument::fromJson(reply.body());
- if (!doc.array().isEmpty()) {
- servers << url;
- if (servers.size() > 4) {
- haveEnoughServers = true;
- std::shuffle(servers.begin(), servers.end(),
- *QRandomGenerator::global());
- qDebug() << servers;
- emit serversInitialized();
- }
+ connect(reply, &HttpReply::finished, this, [this, url](auto &reply) {
+ if (reply.isSuccessful()) {
+ QJsonDocument doc = QJsonDocument::fromJson(reply.body());
+ if (!doc.array().isEmpty()) {
+ if (servers.size() < 3) {
+ servers << url;
+ if (servers.size() == 3) {
+ shuffleServers();
+ for (const auto &s : qAsConst(preferredServers))
+ servers.prepend(s);
+ qDebug() << servers;
+ emit serversInitialized();
}
}
- });
+ }
+ }
+ });
}
}
}
});
}
+void Invidious::shuffleServers() {
+ shuffle(servers);
+}
+
QString Invidious::baseUrl() {
QString host;
- if (servers.isEmpty())
- host = "https://invidious.snopyta.org";
- else
+ if (servers.isEmpty()) {
+ if (preferredServers.isEmpty())
+ host = fallbackServers.first();
+ else
+ host = preferredServers.first();
+ } else
host = servers.first();
+
QString url = host + QLatin1String("/api/v1/");
return url;
}
explicit Invidious(QObject *parent = nullptr);
void initServers();
+ void shuffleServers();
QString baseUrl();
QUrl method(const QString &name);
$$PWD/ivlistparser.h \
$$PWD/ivsearch.h \
$$PWD/ivsinglevideosource.h \
- $$PWD/ivvideolist.h
+ $$PWD/ivvideolist.h \
+ $$PWD/ivvideosource.h
SOURCES += $$PWD/invidious.cpp \
$$PWD/ivchannel.cpp \
$$PWD/ivlistparser.cpp \
$$PWD/ivsearch.cpp \
$$PWD/ivsinglevideosource.cpp \
- $$PWD/ivvideolist.cpp
+ $$PWD/ivvideolist.cpp \
+ $$PWD/ivvideosource.cpp
}
IVChannelSource::IVChannelSource(SearchParams *searchParams, QObject *parent)
- : VideoSource(parent), searchParams(searchParams) {
+ : IVVideoSource(parent), searchParams(searchParams) {
searchParams->setParent(this);
}
-void IVChannelSource::loadVideos(int max, int startIndex) {
- aborted = false;
-
+void IVChannelSource::reallyLoadVideos(int max, int startIndex) {
QUrl url = Invidious::instance().method("channels/videos/");
url.setPath(url.path() + searchParams->channelId());
connect(reply, &HttpReply::data, this, [this](auto data) {
QJsonDocument doc = QJsonDocument::fromJson(data);
const QJsonArray items = doc.array();
+ if (items.isEmpty()) {
+ handleError("No videos");
+ return;
+ }
+
IVListParser parser(items);
const QVector<Video *> &videos = parser.getVideos();
emit gotVideos(videos);
emit finished(videos.size());
});
- connect(reply, &HttpReply::error, this, [this](auto message) {
- Invidious::instance().initServers();
- emit error(message);
- });
-}
-
-void IVChannelSource::abort() {
- aborted = true;
+ connect(reply, &HttpReply::error, this, &IVChannelSource::handleError);
}
QString IVChannelSource::getName() {
#ifndef IVCHANNELSOURCE_H
#define IVCHANNELSOURCE_H
-#include "videosource.h"
+#include "ivvideosource.h"
#include <QtNetwork>
class SearchParams;
-class IVChannelSource : public VideoSource {
+class IVChannelSource : public IVVideoSource {
Q_OBJECT
public:
IVChannelSource(SearchParams *searchParams, QObject *parent = nullptr);
- void loadVideos(int max, int startIndex);
- void abort();
+ void reallyLoadVideos(int max, int startIndex);
QString getName();
const QList<QAction *> &getActions();
int maxResults();
private:
SearchParams *searchParams;
- bool aborted;
QString name;
};
}
IVSearch::IVSearch(SearchParams *searchParams, QObject *parent)
- : VideoSource(parent), searchParams(searchParams) {
+ : IVVideoSource(parent), searchParams(searchParams) {
searchParams->setParent(this);
}
-void IVSearch::loadVideos(int max, int startIndex) {
- aborted = false;
-
+void IVSearch::reallyLoadVideos(int max, int startIndex) {
QUrl url = Invidious::instance().method("search");
QUrlQuery q(url);
url.setQuery(q);
// qWarning() << "YT3 search" << url.toString();
- QObject *reply = Invidious::cachedHttp().get(url);
+ auto reply = Invidious::cachedHttp().get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
- connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+ connect(reply, &HttpReply::error, this, &IVSearch::handleError);
}
void IVSearch::parseResults(const QByteArray &data) {
QJsonDocument doc = QJsonDocument::fromJson(data);
const QJsonArray items = doc.array();
+ if (items.isEmpty()) {
+ handleError("No videos");
+ return;
+ }
+
IVListParser parser(items);
const QVector<Video *> &videos = parser.getVideos();
emit finished(videos.size());
}
-void IVSearch::abort() {
- aborted = true;
-}
-
QString IVSearch::getName() {
if (!name.isEmpty()) return name;
if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
return QString();
}
-void IVSearch::requestError(const QString &message) {
- Invidious::instance().initServers();
- QString msg = message;
- emit error(msg);
-}
-
const QList<QAction *> &IVSearch::getActions() {
static const QList<QAction *> channelActions = {
MainWindow::instance()->getAction("subscribeChannel")};
#ifndef IVSEARCH_H
#define IVSEARCH_H
-#include "videosource.h"
+#include "ivvideosource.h"
#include <QtNetwork>
class SearchParams;
class Video;
-class IVSearch : public VideoSource {
+class IVSearch : public IVVideoSource {
Q_OBJECT
public:
IVSearch(SearchParams *params, QObject *parent = 0);
- void loadVideos(int max, int startIndex);
- void abort();
+ void reallyLoadVideos(int max, int startIndex);
QString getName();
const QList<QAction *> &getActions();
int maxResults();
private slots:
void parseResults(const QByteArray &data);
- void requestError(const QString &message);
private:
SearchParams *searchParams;
- bool aborted;
QString name;
};
#include "ivlistparser.h"
IVSingleVideoSource::IVSingleVideoSource(QObject *parent)
- : VideoSource(parent), video(nullptr), startIndex(0), max(0) {}
+ : IVVideoSource(parent), video(nullptr), startIndex(0), max(0) {}
-void IVSingleVideoSource::loadVideos(int max, int startIndex) {
+void IVSingleVideoSource::reallyLoadVideos(int max, int startIndex) {
aborted = false;
this->startIndex = startIndex;
this->max = max;
url.setPath(url.path() + "/" + videoId);
}
- QObject *reply = Invidious::cachedHttp().get(url);
+ auto reply = Invidious::cachedHttp().get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
- connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+ connect(reply, &HttpReply::error, this, &IVSingleVideoSource::handleError);
}
void IVSingleVideoSource::parseResults(QByteArray data) {
QJsonDocument doc = QJsonDocument::fromJson(data);
const QJsonArray items = doc.object()["recommendedVideos"].toArray();
+
IVListParser parser(items);
const QVector<Video *> &videos = parser.getVideos();
emit finished(videos.size());
}
-void IVSingleVideoSource::abort() {
- aborted = true;
-}
-
QString IVSingleVideoSource::getName() {
return name;
}
this->video = video;
videoId = video->getId();
}
-
-void IVSingleVideoSource::requestError(const QString &message) {
- emit error(message);
-}
#include <QtCore>
-#include "videosource.h"
+#include "ivvideosource.h"
-class IVSingleVideoSource : public VideoSource {
+class IVSingleVideoSource : public IVVideoSource {
Q_OBJECT
public:
IVSingleVideoSource(QObject *parent = 0);
- void loadVideos(int max, int startIndex);
- void abort();
+ void reallyLoadVideos(int max, int startIndex);
QString getName();
void setVideoId(const QString &value) { videoId = value; }
private slots:
void parseResults(QByteArray data);
- void requestError(const QString &message);
private:
Video *video;
QString videoId;
- bool aborted;
int startIndex;
int max;
QString name;
#include "video.h"
IVVideoList::IVVideoList(const QString &req, const QString &name, QObject *parent)
- : VideoSource(parent), name(name), req(req) {}
+ : IVVideoSource(parent), name(name), req(req) {}
-void IVVideoList::loadVideos(int max, int startIndex) {
+void IVVideoList::reallyLoadVideos(int max, int startIndex) {
aborted = false;
QUrl url(Invidious::instance().baseUrl() + req);
connect(reply, &HttpReply::data, this, [this](auto data) {
QJsonDocument doc = QJsonDocument::fromJson(data);
const QJsonArray items = doc.array();
+ if (items.isEmpty()) {
+ handleError("No videos");
+ return;
+ }
+
IVListParser parser(items);
const QVector<Video *> &videos = parser.getVideos();
- qDebug() << "CAOCAO" << req << name << videos.size();
emit gotVideos(videos);
emit finished(videos.size());
});
- connect(reply, &HttpReply::error, this, [this](auto message) {
- Invidious::instance().initServers();
- emit error(message);
- });
-}
-
-void IVVideoList::abort() {
- aborted = true;
+ connect(reply, &HttpReply::error, this, &IVVideoList::handleError);
}
#ifndef IVVIDEOLIST_H
#define IVVIDEOLIST_H
-#include "videosource.h"
+#include "ivvideosource.h"
#include <QtCore>
-class IVVideoList : public VideoSource {
+class IVVideoList : public IVVideoSource {
Q_OBJECT
public:
IVVideoList(const QString &req, const QString &name, QObject *parent = nullptr);
- void loadVideos(int max, int startIndex);
- void abort();
+ void reallyLoadVideos(int max, int startIndex);
QString getName() { return name; };
bool hasMoreVideos() { return false; }
private:
- bool aborted;
QString name;
QString req;
};
--- /dev/null
+#include "ivvideosource.h"
+
+void IVVideoSource::loadVideos(int max, int startIndex) {
+ aborted = false;
+ retryCount = 0;
+ this->max = max;
+ this->startIndex = startIndex;
+ reallyLoadVideos(max, startIndex);
+}
+
+void IVVideoSource::abort() {
+ aborted = true;
+ retryCount = 0;
+ max = 0;
+ startIndex = 0;
+}
+
+void IVVideoSource::handleError(QString message) {
+ qDebug() << message;
+ if (retryCount < 4) {
+ qDebug() << "Retrying...";
+ Invidious::instance().shuffleServers();
+ reallyLoadVideos(max, startIndex);
+ retryCount++;
+ } else {
+ qWarning() << message;
+ emit error("Error loading videos");
+ }
+}
--- /dev/null
+#ifndef IVVIDEOSOURCE_H
+#define IVVIDEOSOURCE_H
+
+#include <QtCore>
+
+#include "invidious.h"
+#include "videosource.h"
+
+class IVVideoSource : public VideoSource {
+ Q_OBJECT
+
+public:
+ IVVideoSource(QObject *parent = nullptr) : VideoSource(parent) {}
+
+ void loadVideos(int max, int startIndex);
+ void abort();
+
+ virtual void reallyLoadVideos(int max, int startIndex) = 0;
+
+protected slots:
+ void handleError(QString message);
+
+protected:
+ bool aborted = false;
+
+private:
+ int retryCount = 0;
+ int max = 0;
+ int startIndex = 0;
+};
+
+#endif // IVVIDEOSOURCE_H
if (keys.isEmpty()) {
qWarning() << "No available API keys";
-#ifdef APP_LINUX
+#ifdef APP_LINUX_NO
QMetaObject::invokeMethod(MainWindow::instance(), "missingKeyWarning",
Qt::QueuedConnection);
#endif