<source>Please license %1</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <source>This demo has expired.</source>
- <translation type="unfinished"></translation>
- </message>
<message>
<source>The full version allows you to watch videos without interruptions.</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <source>Without a license, the application will expire in %1 days.</source>
- <translation type="unfinished"></translation>
- </message>
<message>
<source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
<translation type="unfinished"></translation>
<source>Subscribed to %1</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Rewind %1 seconds</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Fast forward %1 seconds</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>channel</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>MessageWidget</name>
<source>Get the full version</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Need a remote control for %1? Try %2!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>I keep improving %1 to make it the best I can. Support this work!</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>SidebarHeader</name>
HEADERS += \
$$PWD/src/cachedhttp.h \
$$PWD/src/http.h \
+ $$PWD/src/httpreply.h \
+ $$PWD/src/httprequest.h \
$$PWD/src/localcache.h \
+ $$PWD/src/networkhttpreply.h \
$$PWD/src/throttledhttp.h
SOURCES += \
$$PWD/src/cachedhttp.cpp \
$$PWD/src/http.cpp \
+ $$PWD/src/httpreply.cpp \
$$PWD/src/localcache.cpp \
+ $$PWD/src/networkhttpreply.cpp \
$$PWD/src/throttledhttp.cpp
}
void WrappedHttpReply::originFinished(const HttpReply &reply) {
+ qDebug() << reply.statusCode() << reply.url();
if (reply.isSuccessful()) cache->insert(key, reply.body());
emit finished(reply);
}
const QByteArray key = requestHash(req);
const QByteArray value = cache->value(key);
if (!value.isNull()) {
- qDebug() << "CachedHttp HIT" << req.url;
+ qDebug() << "HIT" << key << req.url;
return new CachedHttpReply(value, req);
}
- qDebug() << "CachedHttp MISS" << req.url.toString();
+ qDebug() << "MISS" << key << req.url;
return new WrappedHttpReply(cache, key, http.request(req));
}
#include "http.h"
-namespace {
+#include "networkhttpreply.h"
-QNetworkAccessManager *createNetworkAccessManager() {
- QNetworkAccessManager *nam = new QNetworkAccessManager();
- return nam;
-}
+namespace {
QNetworkAccessManager *networkAccessManager() {
- static QMap<QThread *, QNetworkAccessManager *> nams;
- QThread *t = QThread::currentThread();
- QMap<QThread *, QNetworkAccessManager *>::const_iterator i = nams.constFind(t);
- if (i != nams.constEnd()) return i.value();
- QNetworkAccessManager *nam = createNetworkAccessManager();
- nams.insert(t, nam);
+ static thread_local QNetworkAccessManager *nam = new QNetworkAccessManager();
return nam;
}
int defaultReadTimeout = 10000;
+int defaultMaxRetries = 3;
} // namespace
-Http::Http() : requestHeaders(getDefaultRequestHeaders()), readTimeout(defaultReadTimeout) {}
+Http::Http()
+ : requestHeaders(getDefaultRequestHeaders()), readTimeout(defaultReadTimeout),
+ maxRetries(defaultMaxRetries) {}
void Http::setRequestHeaders(const QMap<QByteArray, QByteArray> &headers) {
requestHeaders = headers;
request.setRawHeader(it.key(), it.value());
if (req.offset > 0)
- request.setRawHeader("Range", QString("bytes=%1-").arg(req.offset).toUtf8());
+ request.setRawHeader("Range", QStringLiteral("bytes=%1-").arg(req.offset).toUtf8());
QNetworkAccessManager *manager = networkAccessManager();
return request(req);
}
-NetworkHttpReply::NetworkHttpReply(const HttpRequest &req, Http &http)
- : http(http), req(req), retryCount(0) {
- if (req.url.isEmpty()) {
- qWarning() << "Empty URL";
- }
-
- networkReply = http.networkReply(req);
- setParent(networkReply);
- setupReply();
-
- readTimeoutTimer = new QTimer(this);
- readTimeoutTimer->setInterval(http.getReadTimeout());
- readTimeoutTimer->setSingleShot(true);
- connect(readTimeoutTimer, SIGNAL(timeout()), SLOT(readTimeout()), Qt::UniqueConnection);
- readTimeoutTimer->start();
-}
-
-void NetworkHttpReply::setupReply() {
- connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)),
- SLOT(replyError(QNetworkReply::NetworkError)), Qt::UniqueConnection);
- connect(networkReply, SIGNAL(finished()), SLOT(replyFinished()), Qt::UniqueConnection);
- connect(networkReply, SIGNAL(downloadProgress(qint64, qint64)),
- SLOT(downloadProgress(qint64, qint64)), Qt::UniqueConnection);
-}
-
-QString NetworkHttpReply::errorMessage() {
- return url().toString() + QLatin1Char(' ') + QString::number(statusCode()) + QLatin1Char(' ') +
- reasonPhrase();
-}
-
-void NetworkHttpReply::emitError() {
- const QString msg = errorMessage();
-#ifndef QT_NO_DEBUG_OUTPUT
- qDebug() << "Http:" << msg;
- if (!req.body.isEmpty()) qDebug() << "Http:" << req.body;
-#endif
- emit error(msg);
- emitFinished();
-}
-
-void NetworkHttpReply::emitFinished() {
- readTimeoutTimer->stop();
-
- // disconnect to avoid replyFinished() from being called
- networkReply->disconnect();
-
- emit finished(*this);
-
- // bye bye my reply
- // this will also delete this object and HttpReply as the QNetworkReply is their parent
- networkReply->deleteLater();
-}
-
-void NetworkHttpReply::replyFinished() {
- QUrl redirection = networkReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
- if (redirection.isValid()) {
- HttpRequest redirectReq;
- redirectReq.url = redirection;
- redirectReq.operation = req.operation;
- redirectReq.body = req.body;
- redirectReq.offset = req.offset;
- QNetworkReply *redirectReply = http.networkReply(redirectReq);
- setParent(redirectReply);
- networkReply->deleteLater();
- networkReply = redirectReply;
- setupReply();
- readTimeoutTimer->start();
- return;
- }
-
- if (isSuccessful()) {
- bytes = networkReply->readAll();
- emit data(bytes);
-
-#ifndef QT_NO_DEBUG_OUTPUT
- if (!networkReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool())
- qDebug() << networkReply->url().toString() << statusCode();
- else
- qDebug() << "CACHE" << networkReply->url().toString();
-#endif
- }
-
- emitFinished();
-}
-
-void NetworkHttpReply::replyError(QNetworkReply::NetworkError code) {
- Q_UNUSED(code);
- const int status = statusCode();
- if (retryCount <= 3 && status >= 500 && status < 600) {
- qDebug() << "Retrying" << req.url;
- networkReply->disconnect();
- networkReply->deleteLater();
- QNetworkReply *retryReply = http.networkReply(req);
- setParent(retryReply);
- networkReply = retryReply;
- setupReply();
- retryCount++;
- readTimeoutTimer->start();
- } else {
- emitError();
- return;
- }
-}
-
-void NetworkHttpReply::downloadProgress(qint64 bytesReceived, qint64 /* bytesTotal */) {
- // qDebug() << "Downloading" << bytesReceived << bytesTotal << networkReply->url();
- if (bytesReceived > 0 && readTimeoutTimer->isActive()) {
- readTimeoutTimer->stop();
- disconnect(networkReply, SIGNAL(downloadProgress(qint64, qint64)), this,
- SLOT(downloadProgress(qint64, qint64)));
- }
-}
-
-void NetworkHttpReply::readTimeout() {
- if (!networkReply) return;
- networkReply->disconnect();
- networkReply->abort();
- networkReply->deleteLater();
-
- if (retryCount > 3 && (networkReply->operation() != QNetworkAccessManager::GetOperation &&
- networkReply->operation() != QNetworkAccessManager::HeadOperation)) {
- emitError();
- emit finished(*this);
- return;
- }
-
- qDebug() << "Timeout" << req.url;
- QNetworkReply *retryReply = http.networkReply(req);
- setParent(retryReply);
- networkReply = retryReply;
- setupReply();
- retryCount++;
- readTimeoutTimer->start();
-}
-
-QUrl NetworkHttpReply::url() const {
- return networkReply->url();
-}
-
-int NetworkHttpReply::statusCode() const {
- return networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-}
-
-QString NetworkHttpReply::reasonPhrase() const {
- return networkReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
-}
-
-const QList<QNetworkReply::RawHeaderPair> NetworkHttpReply::headers() const {
- return networkReply->rawHeaderPairs();
-}
-
-QByteArray NetworkHttpReply::header(const QByteArray &headerName) const {
- return networkReply->rawHeader(headerName);
+int Http::getMaxRetries() const
+{
+ return maxRetries;
}
-QByteArray NetworkHttpReply::body() const {
- return bytes;
+void Http::setMaxRetries(int value)
+{
+ maxRetries = value;
}
#include <QtNetwork>
-class HttpRequest {
-public:
- HttpRequest() : operation(QNetworkAccessManager::GetOperation), offset(0) {}
- QUrl url;
- QNetworkAccessManager::Operation operation;
- QByteArray body;
- uint offset;
- QMap<QByteArray, QByteArray> headers;
-};
-
-class HttpReply : public QObject {
- Q_OBJECT
-
-public:
- HttpReply(QObject *parent = nullptr) : QObject(parent) {}
- virtual QUrl url() const = 0;
- virtual int statusCode() const = 0;
- int isSuccessful() const { return statusCode() >= 200 && statusCode() < 300; }
- virtual QString reasonPhrase() const { return QString(); }
- virtual const QList<QNetworkReply::RawHeaderPair> headers() const {
- return QList<QNetworkReply::RawHeaderPair>();
- }
- virtual QByteArray header(const QByteArray &headerName) const {
- Q_UNUSED(headerName);
- return QByteArray();
- }
-
- virtual QByteArray body() const = 0;
-
-signals:
- void data(const QByteArray &bytes);
- void error(const QString &message);
- void finished(const HttpReply &reply);
-};
+#include "httpreply.h"
+#include "httprequest.h"
class Http {
public:
void setReadTimeout(int timeout);
int getReadTimeout() { return readTimeout; }
+ int getMaxRetries() const;
+ void setMaxRetries(int value);
+
QNetworkReply *networkReply(const HttpRequest &req);
virtual HttpReply *request(const HttpRequest &req);
HttpReply *
private:
QMap<QByteArray, QByteArray> requestHeaders;
int readTimeout;
-};
-
-class NetworkHttpReply : public HttpReply {
- Q_OBJECT
-
-public:
- NetworkHttpReply(const HttpRequest &req, Http &http);
- QUrl url() const;
- int statusCode() const;
- QString reasonPhrase() const;
- const QList<QNetworkReply::RawHeaderPair> headers() const;
- QByteArray header(const QByteArray &headerName) const;
- QByteArray body() const;
-
-private slots:
- void replyFinished();
- void replyError(QNetworkReply::NetworkError);
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
- void readTimeout();
-
-private:
- void setupReply();
- QString errorMessage();
- void emitError();
- void emitFinished();
-
- Http &http;
- HttpRequest req;
- QNetworkReply *networkReply;
- QTimer *readTimeoutTimer;
- int retryCount;
- QByteArray bytes;
+ int maxRetries;
};
#endif // HTTP_H
--- /dev/null
+#include "httpreply.h"
+
+HttpReply::HttpReply(QObject *parent) : QObject(parent) {}
+
+int HttpReply::isSuccessful() const {
+ return statusCode() >= 200 && statusCode() < 300;
+}
+
+QString HttpReply::reasonPhrase() const {
+ return QString();
+}
+
+const QList<QNetworkReply::RawHeaderPair> HttpReply::headers() const {
+ return QList<QNetworkReply::RawHeaderPair>();
+}
+
+QByteArray HttpReply::header(const QByteArray &headerName) const {
+ Q_UNUSED(headerName);
+ return QByteArray();
+}
--- /dev/null
+#ifndef HTTPREPLY_H
+#define HTTPREPLY_H
+
+#include <QtNetwork>
+
+class HttpReply : public QObject {
+ Q_OBJECT
+
+public:
+ HttpReply(QObject *parent = nullptr);
+ virtual QUrl url() const = 0;
+ virtual int statusCode() const = 0;
+ int isSuccessful() const;
+ virtual QString reasonPhrase() const;
+ virtual const QList<QNetworkReply::RawHeaderPair> headers() const;
+ virtual QByteArray header(const QByteArray &headerName) const;
+ virtual QByteArray body() const = 0;
+
+signals:
+ void data(const QByteArray &bytes);
+ void error(const QString &message);
+ void finished(const HttpReply &reply);
+};
+
+#endif // HTTPREPLY_H
--- /dev/null
+#ifndef HTTPREQUEST_H
+#define HTTPREQUEST_H
+
+#include <QtNetwork>
+
+class HttpRequest {
+public:
+ HttpRequest() : operation(QNetworkAccessManager::GetOperation), offset(0) {}
+ QUrl url;
+ QNetworkAccessManager::Operation operation;
+ QByteArray body;
+ uint offset;
+ QMap<QByteArray, QByteArray> headers;
+};
+
+#endif // HTTPREQUEST_H
}
LocalCache::LocalCache(const QByteArray &name)
- : name(name), maxSeconds(86400 * 30), maxSize(1024 * 1024 * 100), size(0), expiring(false),
- insertCount(0) {
+ : name(name), maxSeconds(86400 * 30), maxSize(1024 * 1024 * 100), size(0), insertCount(0) {
directory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') +
QLatin1String(name) + QLatin1Char('/');
#ifndef QT_NO_DEBUG_OUTPUT
}
bool LocalCache::isCached(const QString &path) {
- bool cached = (QFile::exists(path) &&
- (maxSeconds == 0 || QDateTime::currentDateTimeUtc().toTime_t() -
- QFileInfo(path).created().toTime_t() <
- maxSeconds));
+ bool cached = QFile::exists(path) &&
+ (maxSeconds == 0 || QFileInfo(path).birthTime().secsTo(
+ QDateTime::currentDateTimeUtc()) < maxSeconds);
#ifndef QT_NO_DEBUG_OUTPUT
if (!cached) misses++;
#endif
}
void LocalCache::insert(const QByteArray &key, const QByteArray &value) {
- const QueueItem item = {key, value};
- insertQueue.append(item);
- QTimer::singleShot(0, [this]() {
- if (insertQueue.isEmpty()) return;
- for (const auto &item : insertQueue) {
- const QString path = cachePath(item.key);
- const QString parentDir = path.left(path.lastIndexOf('/'));
- if (!QFile::exists(parentDir)) {
- QDir().mkpath(parentDir);
- }
- QFile file(path);
- if (!file.open(QIODevice::WriteOnly)) {
- qWarning() << "Cannot create" << path;
- continue;
- }
- file.write(item.value);
- file.close();
- if (size > 0) size += item.value.size();
- }
- insertQueue.clear();
-
- // expire cache every n inserts
- if (maxSize > 0 && ++insertCount % 100 == 0) {
- if (size == 0 || size > maxSize) size = expire();
- }
- });
+ qDebug() << "Inserting" << key;
+ const QString path = cachePath(key);
+ const QString parentDir = path.left(path.lastIndexOf(QLatin1Char('/')));
+ if (!QFile::exists(parentDir)) {
+ QDir().mkpath(parentDir);
+ }
+ QFile file(path);
+ if (!file.open(QIODevice::WriteOnly)) {
+ qWarning() << "Cannot create" << path;
+ return;
+ }
+ file.write(value);
+ file.close();
+ if (size > 0) size += value.size();
+
+ // expire cache every n inserts
+ if (maxSize > 0 && ++insertCount % 100 == 0) {
+ if (size == 0 || size > maxSize) expire();
+ }
}
-bool LocalCache::clear() {
+void LocalCache::clear() {
#ifndef QT_NO_DEBUG_OUTPUT
hits = 0;
misses = 0;
#endif
size = 0;
insertCount = 0;
- return QDir(directory).removeRecursively();
+ mutex.lock();
+ QDir(directory).removeRecursively();
+ mutex.unlock();
}
QString LocalCache::cachePath(const QByteArray &key) const {
- return directory + QLatin1String(key.constData());
+ return directory + QLatin1String(key);
}
-qint64 LocalCache::expire() {
- if (expiring) return size;
- expiring = true;
+void LocalCache::expire() {
+ if (!mutex.tryLock()) return;
QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
QDirIterator it(directory, filters, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString path = it.next();
QFileInfo info = it.fileInfo();
- cacheItems.insert(info.created(), path);
+ cacheItems.insert(info.birthTime(), path);
totalSize += info.size();
qApp->processEvents();
}
}
#endif
- expiring = false;
-
- return totalSize;
+ size = totalSize;
+ mutex.unlock();
}
#ifndef QT_NO_DEBUG_OUTPUT
QByteArray value(const QByteArray &key);
void insert(const QByteArray &key, const QByteArray &value);
- bool clear();
+ void clear();
private:
LocalCache(const QByteArray &name);
QString cachePath(const QByteArray &key) const;
bool isCached(const QString &path);
- qint64 expire();
+ void expire();
#ifndef QT_NO_DEBUG_OUTPUT
void debugStats();
#endif
uint maxSeconds;
qint64 maxSize;
qint64 size;
- bool expiring;
+ QMutex mutex;
uint insertCount;
- struct QueueItem {
- QByteArray key;
- QByteArray value;
- };
- QVector<QueueItem> insertQueue;
#ifndef QT_NO_DEBUG_OUTPUT
uint hits;
--- /dev/null
+#include "networkhttpreply.h"
+
+NetworkHttpReply::NetworkHttpReply(const HttpRequest &req, Http &http)
+ : http(http), req(req), retryCount(0) {
+ if (req.url.isEmpty()) {
+ qWarning() << "Empty URL";
+ }
+
+ networkReply = http.networkReply(req);
+ setParent(networkReply);
+ setupReply();
+
+ readTimeoutTimer = new QTimer(this);
+ readTimeoutTimer->setInterval(http.getReadTimeout());
+ readTimeoutTimer->setSingleShot(true);
+ connect(readTimeoutTimer, SIGNAL(timeout()), SLOT(readTimeout()), Qt::UniqueConnection);
+ readTimeoutTimer->start();
+}
+
+void NetworkHttpReply::setupReply() {
+ connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(replyError(QNetworkReply::NetworkError)), Qt::UniqueConnection);
+ connect(networkReply, SIGNAL(finished()), SLOT(replyFinished()), Qt::UniqueConnection);
+ connect(networkReply, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)), Qt::UniqueConnection);
+}
+
+QString NetworkHttpReply::errorMessage() {
+ return url().toString() + QLatin1Char(' ') + QString::number(statusCode()) + QLatin1Char(' ') +
+ reasonPhrase();
+}
+
+void NetworkHttpReply::emitError() {
+ const QString msg = errorMessage();
+#ifndef QT_NO_DEBUG_OUTPUT
+ qDebug() << "Http:" << msg;
+ if (!req.body.isEmpty()) qDebug() << "Http:" << req.body;
+#endif
+ emit error(msg);
+ emitFinished();
+}
+
+void NetworkHttpReply::emitFinished() {
+ readTimeoutTimer->stop();
+
+ // disconnect to avoid replyFinished() from being called
+ networkReply->disconnect();
+
+ emit finished(*this);
+
+ // bye bye my reply
+ // this will also delete this object and HttpReply as the QNetworkReply is their parent
+ networkReply->deleteLater();
+}
+
+void NetworkHttpReply::replyFinished() {
+ QUrl redirection = networkReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ if (redirection.isValid()) {
+ HttpRequest redirectReq;
+ if (redirection.isRelative()) redirection = networkReply->url().resolved(redirection);
+ redirectReq.url = redirection;
+ qDebug() << "Redirected to" << redirectReq.url;
+ redirectReq.operation = req.operation;
+ redirectReq.body = req.body;
+ redirectReq.offset = req.offset;
+ QNetworkReply *redirectReply = http.networkReply(redirectReq);
+ setParent(redirectReply);
+ networkReply->deleteLater();
+ networkReply = redirectReply;
+ setupReply();
+ readTimeoutTimer->start();
+ return;
+ }
+
+ if (isSuccessful()) {
+ bytes = networkReply->readAll();
+ emit data(bytes);
+
+#ifndef QT_NO_DEBUG_OUTPUT
+ if (!networkReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool())
+ qDebug() << statusCode() << networkReply->url().toString();
+ else
+ qDebug() << "CACHE" << networkReply->url().toString();
+#endif
+ }
+
+ emitFinished();
+}
+
+void NetworkHttpReply::replyError(QNetworkReply::NetworkError code) {
+ Q_UNUSED(code);
+ const int status = statusCode();
+ if (retryCount <= http.getMaxRetries() && status >= 500 && status < 600 &&
+ (networkReply->operation() == QNetworkAccessManager::GetOperation ||
+ networkReply->operation() == QNetworkAccessManager::HeadOperation)) {
+ qDebug() << "Retrying" << status << QVariant(req.operation).toString() << req.url;
+ networkReply->disconnect();
+ networkReply->deleteLater();
+ QNetworkReply *retryReply = http.networkReply(req);
+ setParent(retryReply);
+ networkReply = retryReply;
+ setupReply();
+ retryCount++;
+ readTimeoutTimer->start();
+ } else {
+ emitError();
+ return;
+ }
+}
+
+void NetworkHttpReply::downloadProgress(qint64 bytesReceived, qint64 /* bytesTotal */) {
+ // qDebug() << "Downloading" << bytesReceived << bytesTotal << networkReply->url();
+ if (bytesReceived > 0 && readTimeoutTimer->isActive()) {
+ readTimeoutTimer->stop();
+ disconnect(networkReply, SIGNAL(downloadProgress(qint64, qint64)), this,
+ SLOT(downloadProgress(qint64, qint64)));
+ }
+}
+
+void NetworkHttpReply::readTimeout() {
+ qDebug() << "Timeout" << req.url;
+
+ if (!networkReply) return;
+
+ bool shouldRetry = (networkReply->operation() == QNetworkAccessManager::GetOperation ||
+ networkReply->operation() == QNetworkAccessManager::HeadOperation) &&
+ retryCount < http.getMaxRetries();
+
+ networkReply->disconnect();
+ networkReply->abort();
+ networkReply->deleteLater();
+
+ if (!shouldRetry) {
+ emitError();
+ emit finished(*this);
+ return;
+ }
+
+ retryCount++;
+ QNetworkReply *retryReply = http.networkReply(req);
+ setParent(retryReply);
+ networkReply = retryReply;
+ setupReply();
+ readTimeoutTimer->start();
+}
+
+QUrl NetworkHttpReply::url() const {
+ return networkReply->url();
+}
+
+int NetworkHttpReply::statusCode() const {
+ return networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+}
+
+QString NetworkHttpReply::reasonPhrase() const {
+ return networkReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+}
+
+const QList<QNetworkReply::RawHeaderPair> NetworkHttpReply::headers() const {
+ return networkReply->rawHeaderPairs();
+}
+
+QByteArray NetworkHttpReply::header(const QByteArray &headerName) const {
+ return networkReply->rawHeader(headerName);
+}
+
+QByteArray NetworkHttpReply::body() const {
+ return bytes;
+}
--- /dev/null
+#ifndef NETWORKHTTPREPLY_H
+#define NETWORKHTTPREPLY_H
+
+#include <QtNetwork>
+
+#include "http.h"
+#include "httpreply.h"
+#include "httprequest.h"
+
+class NetworkHttpReply : public HttpReply {
+ Q_OBJECT
+
+public:
+ NetworkHttpReply(const HttpRequest &req, Http &http);
+ QUrl url() const;
+ int statusCode() const;
+ QString reasonPhrase() const;
+ const QList<QNetworkReply::RawHeaderPair> headers() const;
+ QByteArray header(const QByteArray &headerName) const;
+ QByteArray body() const;
+
+private slots:
+ void replyFinished();
+ void replyError(QNetworkReply::NetworkError);
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void readTimeout();
+
+private:
+ void setupReply();
+ QString errorMessage();
+ void emitError();
+ void emitFinished();
+
+ Http &http;
+ HttpRequest req;
+ QNetworkReply *networkReply;
+ QTimer *readTimeoutTimer;
+ int retryCount;
+ QByteArray bytes;
+};
+
+#endif // NETWORKHTTPREPLY_H
connect(timer, SIGNAL(timeout()), SLOT(checkElapsed()));
}
qDebug() << "Throttling" << req.url
- << QString("%1ms").arg(milliseconds - elapsedSinceLastRequest);
+ << QStringLiteral("%1ms").arg(milliseconds - elapsedSinceLastRequest);
timer->setInterval(milliseconds - elapsedSinceLastRequest);
timer->start();
return;
#include "mediampv.h"
#include <clocale>
-#include <mpv/qthelper.hpp>
#ifndef MEDIA_AUDIOONLY
#include "mpvwidget.h"
mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv, 0, "mute", MPV_FORMAT_FLAG);
+ mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
}
// This slot is invoked by wakeup() (through the mpvEvents signal).
}
void MediaMPV::handleMpvEvent(mpv_event *event) {
- // qDebug() << event->data;
+ // qDebug() << event->event_id << event->data;
switch (event->event_id) {
case MPV_EVENT_START_FILE:
clearTrackState();
setState(Media::BufferingState);
break;
- case MPV_EVENT_FILE_LOADED:
- setState(Media::PlayingState);
+ case MPV_EVENT_PLAYBACK_RESTART: {
+ int pause;
+ mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &pause);
+ bool paused = pause == 1;
+ if (paused)
+ setState(Media::PausedState);
+ else
+ setState(Media::PlayingState);
break;
+ }
- case MPV_EVENT_PLAYBACK_RESTART:
- case MPV_EVENT_UNPAUSE:
+ case MPV_EVENT_FILE_LOADED:
setState(Media::PlayingState);
break;
break;
}
- case MPV_EVENT_PAUSE:
- setState(Media::PausedState);
- break;
-
case MPV_EVENT_PROPERTY_CHANGE: {
mpv_event_property *prop = (mpv_event_property *)event->data;
- // qDebug() << prop->name << prop->data;
+ qDebug() << prop->name << prop->data;
if (strcmp(prop->name, "time-pos") == 0) {
if (prop->format == MPV_FORMAT_DOUBLE) {
}
}
+ else if (strcmp(prop->name, "pause") == 0) {
+ if (prop->format == MPV_FORMAT_FLAG) {
+ int pause = *(int *)prop->data;
+ bool paused = pause == 1;
+ if (paused)
+ setState(Media::PausedState);
+ else {
+ int coreIdle;
+ mpv_get_property(mpv, "core-idle", MPV_FORMAT_FLAG, &coreIdle);
+ if (coreIdle == 1)
+ setState(Media::StoppedState);
+ else
+ setState(Media::PlayingState);
+ }
+ }
+ }
+
break;
}
void MediaMPV::setState(Media::State value) {
if (value != currentState) {
+ qDebug() << "State" << value;
currentState = value;
emit stateChanged(currentState);
}
</message>
<message>
<source>Powered by %1</source>
- <translation type="unfinished"/>
+ <translation>Angetrieben von %1</translation>
</message>
<message>
<source>Open-source software</source>
- <translation type="unfinished"/>
+ <translation>Open-Source-Software</translation>
</message>
<message>
<source>Icon designed by %1.</source>
<name>ActivationDialog</name>
<message>
<source>Enter your License Details</source>
- <translation>Geben Sie Ihre Lizenzierungsinformationen ein</translation>
+ <translation>Geben Sie Ihre Lizensierungsinformationen ein</translation>
</message>
<message>
<source>&Email:</source>
</message>
<message numerus="yes">
<source>You have %n new video(s)</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>Sie haben %n neues Video</numerusform><numerusform>Sie haben %n neue Videos</numerusform></translation>
</message>
</context>
<context>
</message>
<message numerus="yes">
<source>%n hour(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>vor %n Stunde</numerusform><numerusform>Vor %n Stunden</numerusform></translation>
</message>
<message numerus="yes">
<source>%n day(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>vor %n Tag</numerusform><numerusform>vor %n Tagen</numerusform></translation>
</message>
<message numerus="yes">
<source>%n month(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>vor %n Monat</numerusform><numerusform>vor %n Monaten</numerusform></translation>
</message>
<message>
<source>K</source>
<comment>K as in Kilo, i.e. thousands</comment>
- <translation type="unfinished"/>
+ <translation>Tsd</translation>
</message>
<message>
<source>M</source>
<comment>M stands for Millions</comment>
- <translation type="unfinished"/>
+ <translation>Mio</translation>
</message>
<message>
<source>B</source>
<comment>B stands for Billions</comment>
- <translation type="unfinished"/>
+ <translation>Mrd</translation>
</message>
<message>
<source>%1 views</source>
</message>
<message numerus="yes">
<source>%n week(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>vor %n Woche</numerusform><numerusform>vor %n Wochen</numerusform></translation>
</message>
</context>
<context>
</message>
<message numerus="yes">
<source>%n Download(s)</source>
- <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+ <translation><numerusform>%n Download</numerusform><numerusform>%n Downloads</numerusform></translation>
</message>
</context>
<context>
</message>
<message>
<source>Toggle &Menu Bar</source>
- <translation type="unfinished"/>
+ <translation>&Menüleiste umschalten</translation>
</message>
<message>
<source>Menu</source>
- <translation type="unfinished"/>
+ <translation>Menü</translation>
</message>
<message>
<source>&Love %1? Rate it!</source>
</message>
<message>
<source>You can still access the menu bar by pressing the ALT key</source>
- <translation type="unfinished"/>
+ <translation>Sie können die Menüleiste weiterhin durch Drücken der ALT-Taste erreichen</translation>
</message>
</context>
<context>
</message>
<message>
<source>Switched to %1</source>
- <translation type="unfinished"/>
+ <translation>Umgeschaltet zu %1</translation>
</message>
<message>
<source>Unsubscribed from %1</source>
<name>PickMessage</name>
<message>
<source>Pick a video</source>
- <translation type="unfinished"/>
+ <translation>Wählen Sie ein Video aus</translation>
</message>
</context>
<context>
</message>
<message>
<source>&Forward</source>
- <translation type="unfinished"/>
+ <translation>&Weiter</translation>
</message>
<message>
<source>Forward to %1</source>
</message>
<message>
<source>Powered by %1</source>
- <translation type="unfinished"/>
+ <translation>%1 によって作られました</translation>
</message>
<message>
<source>Open-source software</source>
- <translation type="unfinished"/>
+ <translation>オープンソース・ソフトウェア</translation>
</message>
<message>
<source>Icon designed by %1.</source>
</message>
<message numerus="yes">
<source>You have %n new video(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 個の新しい動画があります</numerusform></translation>
</message>
</context>
<context>
</message>
<message numerus="yes">
<source>%n hour(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 時間前</numerusform></translation>
</message>
<message numerus="yes">
<source>%n day(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 日前</numerusform></translation>
</message>
<message numerus="yes">
<source>%n month(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n か月前</numerusform></translation>
</message>
<message>
<source>K</source>
<comment>K as in Kilo, i.e. thousands</comment>
- <translation type="unfinished"/>
+ <translation>キロ</translation>
</message>
<message>
<source>M</source>
<comment>M stands for Millions</comment>
- <translation type="unfinished"/>
+ <translation>メガ</translation>
</message>
<message>
<source>B</source>
<comment>B stands for Billions</comment>
- <translation type="unfinished"/>
+ <translation>バイト</translation>
</message>
<message>
<source>%1 views</source>
</message>
<message numerus="yes">
<source>%n week(s) ago</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 週間前</numerusform></translation>
</message>
</context>
<context>
</message>
<message numerus="yes">
<source>%n Download(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 個のダウンロード</numerusform></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<source>&Window</source>
- <translation type="unfinished"/>
+ <translation>ウィンドウ(&W)</translation>
</message>
<message>
<source>&Minimize</source>
- <translation type="unfinished"/>
+ <translation>最小化(&M)</translation>
</message>
<message>
<source>&Stop</source>
</message>
<message>
<source>Restricted Mode</source>
- <translation type="unfinished"/>
+ <translation>制限付きモード</translation>
</message>
<message>
<source>Hide videos that may contain inappropriate content</source>
- <translation type="unfinished"/>
+ <translation>不適切なコンテンツを含む可能性のある動画を非表示にする</translation>
</message>
<message>
<source>Toggle &Menu Bar</source>
- <translation type="unfinished"/>
+ <translation>メニューバーを切り替え(&M)</translation>
</message>
<message>
<source>Menu</source>
- <translation type="unfinished"/>
+ <translation>メニュー</translation>
</message>
<message>
<source>&Love %1? Rate it!</source>
</message>
<message>
<source>You can still access the menu bar by pressing the ALT key</source>
- <translation type="unfinished"/>
+ <translation>ALTキーを押すことでメニューバーにアクセスできます</translation>
</message>
</context>
<context>
</message>
<message>
<source>Switched to %1</source>
- <translation type="unfinished"/>
+ <translation>%1 に切り替えました</translation>
</message>
<message>
<source>Unsubscribed from %1</source>
<name>PickMessage</name>
<message>
<source>Pick a video</source>
- <translation type="unfinished"/>
+ <translation>動画を拾う</translation>
</message>
</context>
<context>
</message>
<message>
<source>&Forward</source>
- <translation type="unfinished"/>
+ <translation>進む(&F)</translation>
</message>
<message>
<source>Forward to %1</source>
</message>
<message>
<source>Downloading %1...</source>
- <translation type="unfinished"/>
+ <translation>%1 をダウンロードしています...</translation>
</message>
</context>
<context>
</message>
<message>
<source>Toggle &Menu Bar</source>
- <translation type="unfinished"/>
+ <translation>Bascula a Barra de &Menu</translation>
</message>
<message>
<source>Menu</source>
- <translation type="unfinished"/>
+ <translation>Menu</translation>
</message>
<message>
<source>&Love %1? Rate it!</source>
</message>
<message>
<source>You can still access the menu bar by pressing the ALT key</source>
- <translation type="unfinished"/>
+ <translation>Podes sempre obter a barra de menu com a tecla ALT</translation>
</message>
</context>
<context>
</message>
<message>
<source>Switched to %1</source>
- <translation type="unfinished"/>
+ <translation>Enviado para %1</translation>
</message>
<message>
<source>Unsubscribed from %1</source>
<name>PickMessage</name>
<message>
<source>Pick a video</source>
- <translation type="unfinished"/>
+ <translation>Escolhe um vídeo</translation>
</message>
</context>
<context>
</message>
<message>
<source>&Forward</source>
- <translation type="unfinished"/>
+ <translation>&Avançar</translation>
</message>
<message>
<source>Forward to %1</source>
CONFIG += c++14 exceptions_off rtti_off optimize_full
TEMPLATE = app
-VERSION = 3.3
+VERSION = 3.4
DEFINES += APP_VERSION="$$VERSION"
APP_NAME = Minitube
DEFINES += APP_SNAPSHOT
-message(Building $${APP_NAME} $${VERSION})
-message(Qt $$[QT_VERSION] in $$[QT_INSTALL_PREFIX])
+CONFIG -= debug_and_release
+CONFIG(debug, debug|release): {
+ message(Building for debug)
+}
+CONFIG(release, debug|release): {
+ message(Building for release)
+ DEFINES *= QT_NO_DEBUG_OUTPUT
+}
-DEFINES *= QT_NO_DEBUG_OUTPUT
-DEFINES *= QT_USE_QSTRINGBUILDER
-DEFINES *= QT_STRICT_ITERATORS
+DEFINES *= QT_USE_QSTRINGBUILDER QT_STRICT_ITERATORS QT_DEPRECATED_WARNINGS
!contains(DEFINES, APP_GOOGLE_API_KEY=.+) {
warning("You need to specify a Google API Key, refer to the README.md file for details")
include(src/qtsingleapplication/qtsingleapplication.pri)
HEADERS += src/video.h \
+ src/messagebar.h \
src/spacer.h \
src/constants.h \
src/playlistitemdelegate.h \
src/videoarea.h \
src/searchlineedit.h
SOURCES += src/main.cpp \
+ src/messagebar.cpp \
src/spacer.cpp \
src/video.cpp \
src/videomimedata.cpp \
}
mac|win32|contains(DEFINES, APP_UBUNTU):include(local/local.pri)
+
+message(CONFIG: $$CONFIG)
+message(DEFINES: $$DEFINES)
+message(QMAKE_CXXFLAGS: $$QMAKE_CXXFLAGS)
+message(QMAKE_LFLAGS: $$QMAKE_LFLAGS)
const QString ext = "deb";
#endif
+ setupApp("Sofa", "sofa." + ext);
setupApp("Finetune", "finetune." + ext);
setupApp("Musictube", "musictube." + ext);
setupApp("Musique", "musique." + ext);
if (re.indexIn(bytes) != -1) {
QString videoId = re.cap(1);
QString latestVideoId = currentChannel->latestVideoId();
- // qDebug() << "Comparing" << videoId << latestVideoId;
+ qDebug() << "Comparing" << videoId << latestVideoId;
hasNewVideos = videoId != latestVideoId;
}
if (hasNewVideos) {
void ClickableLabel::mouseReleaseEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton && rect().contains(e->pos())) emit clicked();
+ QLabel::mouseReleaseEvent(e);
}
void ClickableLabel::leaveEvent(QEvent *e) {
return *h;
}
+Http &HttpUtils::stealthAndNotCached() {
+ static Http *h = [] {
+ Http *http = new Http;
+ http->addRequestHeader("User-Agent", stealthUserAgent());
+
+ return http;
+ }();
+ return *h;
+}
+
void HttpUtils::clearCaches() {
LocalCache::instance("yt")->clear();
LocalCache::instance("http")->clear();
static Http ¬Cached();
static Http &cached();
static Http &yt();
+ static Http &stealthAndNotCached();
static void clearCaches();
static const QByteArray &userAgent();
#include <QAction>
namespace {
+
void addIconFile(QIcon &icon,
const QString &filename,
int size,
}
QIcon IconUtils::fromResources(const char *name) {
+ qDebug() << "Creating icon" << name;
+ static const QLatin1String normal("_normal");
static const QLatin1String active("_active");
static const QLatin1String selected("_selected");
static const QLatin1String disabled("_disabled");
static const QLatin1String checked("_checked");
static const QLatin1String ext(".png");
- QString path(":/icons/");
+ QString path = QStringLiteral(":/icons/");
if (MainWindow::instance()->palette().window().color().value() > 128)
path += QLatin1String("light/");
// WARN keep these sizes updated with what we really use
for (int size : {16, 24, 32, 88}) {
- const QString pathAndName = path + QString::number(size) + '/' + name;
- QString iconFilename = pathAndName + ext;
+ const QString pathAndName =
+ path + QString::number(size) + QLatin1Char('/') + QLatin1String(name);
+ QString iconFilename = pathAndName + normal + ext;
if (QFile::exists(iconFilename)) {
addIconFile(icon, iconFilename, size);
addIconFile(icon, pathAndName + active + ext, size, QIcon::Active);
}
QIcon IconUtils::icon(const char *name) {
-#ifdef APP_LINUX
- QIcon icon = fromTheme(name);
+ static QMap<QByteArray, QIcon> cache = [] {
+ qDebug() << "Init icon cache";
+ QMap<QByteArray, QIcon> c;
+ QObject::connect(qApp, &QApplication::paletteChanged, qApp, [&c]() {
+ qDebug() << "Clearing icon cache";
+ c.clear();
+ });
+ return c;
+ }();
+
+ auto i = cache.constFind(QByteArray::fromRawData(name, strlen(name)));
+ if (i != cache.constEnd()) return i.value();
+
+ QIcon icon;
+#ifdef APP_UBUNTU_NO
+ icon = fromTheme(name);
if (icon.isNull()) icon = fromResources(name);
- return icon;
#else
- return fromResources(name);
+ icon = fromResources(name);
#endif
+
+ cache.insert(QByteArray(name), icon);
+ return icon;
}
QIcon IconUtils::icon(const QVector<const char *> &names) {
int size,
const QColor &background,
const qreal pixelRatio) {
- QString path(":/icons/");
+ QString path = QStringLiteral(":/icons/");
if (background.value() > 128)
- path += "light/";
+ path += QLatin1String("light/");
else
- path += "dark/";
- path += QString::number(size) + '/' + name + QLatin1String(".png");
+ path += QLatin1String("dark/");
+ path += QString::number(size) + QLatin1Char('/') + QLatin1String(name) +
+ QLatin1String("_normal.png");
return IconUtils::pixmap(path, pixelRatio);
}
painter.fillRect(pixmap.rect(), color);
}
+QPixmap IconUtils::pixmap(const char *name, const qreal pixelRatio) {
+ return pixmap(QString::fromLatin1(name), pixelRatio);
+}
+
QPixmap IconUtils::pixmap(const QString &filename, const qreal pixelRatio) {
// Check if a "@2x" file exists
if (pixelRatio > 1.0) {
QIcon i = icon(name);
obj->setIcon(i);
obj->connect(qApp, &QGuiApplication::paletteChanged, obj, [obj, name] {
+ qDebug() << "Updating icon" << name;
QIcon i = icon(name);
obj->setIcon(i);
});
static QIcon tintedIcon(const char *name, const QColor &color, const QSize &size);
// HiDPI stuff
+ static QPixmap pixmap(const char *name, const qreal pixelRatio);
static QPixmap pixmap(const QString &filename, const qreal pixelRatio);
static void tint(QPixmap &pixmap,
QDateTime::currentDateTime().toTime_t() - 1800;
if (stale) loadJs();
} else {
+ /*
QFile resFile(QLatin1String(":/") + jsFilename());
resFile.open(QIODevice::ReadOnly | QIODevice::Text);
parseJs(QString::fromUtf8(resFile.readAll()));
+ */
loadJs();
}
}
void JsFunctions::parseJs(const QString &js) {
+ // qDebug() << "Js Parsing" << js;
if (js.isEmpty()) return;
- // qDebug() << "Parsing" << js;
if (engine) delete engine;
engine = new QJSEngine(this);
engine->evaluate(js);
}
void JsFunctions::loadJs() {
+ qDebug() << "Js Loading" << url;
QUrl url(this->url);
QUrlQuery q;
q.addQueryItem("v", Constants::VERSION);
qSetMessagePattern("[%{function}] %{message}");
#endif
- // Seed random number generator
- qsrand(QDateTime::currentDateTime().toTime_t());
-
#ifdef MEDIA_MPV
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
#ifdef APP_MAC
mac::MacMain();
#endif
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
QtSingleApplication app(argc, argv);
QString message;
if (app.arguments().size() > 1) {
// add actions to the MainWindow so that they work
// when the menu is hidden
addAction(action);
- MainWindow::instance()->setupAction(action);
+ setupAction(action);
}
}
}
void MainWindow::donate() {
- QUrl url(QString(Constants::WEBSITE) + "#donate");
+ QUrl url("https://" + QLatin1String(Constants::ORG_DOMAIN) + "/donate");
showMessage(QString(tr("Opening %1").arg(url.toString())));
QDesktopServices::openUrl(url);
}
ChannelAggregator::instance()->stop();
ChannelAggregator::instance()->cleanup();
Database::shutdown();
- HttpUtils::clearCaches();
qApp->quit();
}
#include "videoarea.h"
#ifdef APP_ACTIVATION
#include "activation.h"
+#include "activationview.h"
#endif
#ifdef APP_EXTRA
#include "extra.h"
#ifdef APP_ACTIVATION
demoTimer = new QTimer(this);
demoTimer->setSingleShot(true);
- connect(demoTimer, &QTimer::timeout, mainWindow, &MainWindow::showActivationView,
+ connect(
+ demoTimer, &QTimer::timeout, this,
+ [this] {
+ if (media->state() != Media::PlayingState) return;
+ media->pause();
+ connect(
+ ActivationView::instance(), &ActivationView::done, media,
+ [this] { media->play(); }, Qt::UniqueConnection);
+ MainWindow::instance()->showActivationView();
+ },
Qt::QueuedConnection);
#endif
for (auto *name : videoActionNames) {
currentVideoActions.append(mainWindow->getAction(name));
}
+
+ for (int i = 0; i < 10; ++i) {
+ QAction *action = new QAction(QString());
+ action->setShortcut(Qt::Key_0 + i);
+ action->setAutoRepeat(false);
+ connect(action, &QAction::triggered, this, [this, i] {
+ qint64 duration = media->duration();
+ // dur : pos = 100 : i*10
+ qint64 position = (duration * (i * 10)) / 100;
+ media->seek(position);
+ });
+ addAction(action);
+ playingVideoActions << action;
+ }
+
+ QAction *leftAction = new QAction(tr("Rewind %1 seconds").arg(10));
+ leftAction->setShortcut(Qt::Key_Left);
+ leftAction->setAutoRepeat(false);
+ connect(leftAction, &QAction::triggered, this, [this] {
+ qint64 position = media->position();
+ position -= 10000;
+ if (position < 0) position = 0;
+ media->seek(position);
+ });
+ addAction(leftAction);
+ playingVideoActions << leftAction;
+
+ QAction *rightAction = new QAction(tr("Fast forward %1 seconds").arg(10));
+ rightAction->setShortcut(Qt::Key_Right);
+ rightAction->setAutoRepeat(false);
+ connect(rightAction, &QAction::triggered, this, [this] {
+ qint64 position = media->position();
+ position += 10000;
+ qint64 duration = media->duration();
+ if (position > duration) position = duration;
+ media->seek(position);
+ });
+ addAction(rightAction);
+ playingVideoActions << rightAction;
}
void MediaView::setMedia(Media *media) {
handleError(media->errorString());
}
+ bool enablePlayingVideoActions = state == Media::PlayingState || state == Media::PausedState;
+ for (QAction *action : qAsConst(playingVideoActions))
+ action->setEnabled(enablePlayingVideoActions);
+
if (state == Media::PlayingState) {
bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
}
#ifdef APP_ACTIVATION
- if (!Activation::instance().isActivated() && !demoTimer->isActive()) {
- int ms = (60000 * 5) + (qrand() % (60000 * 5));
+ if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
+ int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
demoTimer->start(ms);
}
#endif
QVector<VideoSource *> history;
QVector<QAction *> currentVideoActions;
+ QVector<QAction *> playingVideoActions;
qint64 currentVideoSize;
--- /dev/null
+#include "messagebar.h"
+#include "iconutils.h"
+
+MessageBar::MessageBar(QWidget *parent) : QWidget(parent) {
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+
+ QBoxLayout *layout = new QHBoxLayout(this);
+ layout->setSpacing(16);
+
+ msgLabel = new QLabel();
+ msgLabel->setOpenExternalLinks(true);
+ layout->addWidget(msgLabel);
+
+ QToolButton *closeToolButton = new QToolButton();
+ closeToolButton->setIcon(IconUtils::icon("close"));
+ connect(closeToolButton, &QToolButton::clicked, this, [this] {
+ emit closed();
+ hide();
+ });
+ layout->addWidget(closeToolButton);
+}
+
+void MessageBar::setMessage(const QString &message) {
+ msgLabel->setText(message);
+}
+
+void MessageBar::paintEvent(QPaintEvent *e) {
+ Q_UNUSED(e);
+ QStyleOption o;
+ o.initFrom(this);
+ QPainter p(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this);
+}
--- /dev/null
+#ifndef MESSAGEBAR_H
+#define MESSAGEBAR_H
+
+#include <QtWidgets>
+
+class MessageBar : public QWidget {
+ Q_OBJECT
+
+public:
+ MessageBar(QWidget *parent = 0);
+ void setMessage(const QString &message);
+
+signals:
+ void closed();
+
+protected:
+ void paintEvent(QPaintEvent *e);
+
+private:
+ QLabel *msgLabel;
+};
+
+#endif // MESSAGEBAR_H
#include "clickablelabel.h"
#include "iconutils.h"
#include "mainwindow.h"
+#include "messagebar.h"
#include "painterutils.h"
namespace {
vLayout->setMargin(padding);
vLayout->setSpacing(0);
- // hidden message widget
- message = new QLabel(this);
- message->hide();
- vLayout->addWidget(message);
+ messageBar = new MessageBar();
+ messageBar->hide();
+ vLayout->addWidget(messageBar, 0, Qt::AlignCenter);
+ vLayout->addSpacing(padding);
+ maybeShowMessage();
vLayout->addStretch();
ClickableLabel *item;
if (recentKeywordsLayout->count() - 1 >= counter) {
item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
-
} else {
item = new ClickableLabel();
#ifdef APP_MAC
}
}
});
+ item->setContextMenuPolicy(Qt::ActionsContextMenu);
+ auto removeAction = new QAction(tr("Remove"));
+ item->addAction(removeAction);
+ connect(removeAction, &QAction::triggered, item, [item] {
+ QSettings settings;
+ QStringList keywords = settings.value(recentKeywordsKey).toStringList();
+ QString keyword = item->property("keyword").toString();
+ keywords.removeOne(keyword);
+ settings.setValue(recentKeywordsKey, keywords);
+ item->deleteLater();
+ });
recentKeywordsLayout->addWidget(item);
}
item->setStatusTip(link);
else
item->setStatusTip(QString());
+ item->setProperty("keyword", keyword);
disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
}
ClickableLabel *item = new ClickableLabel(display);
+ item->setProperty("keyword", keyword);
#ifdef APP_MAC
item->setPalette(p);
#endif
connect(item, &ClickableLabel::hovered, item, [item](bool value) {
item->setForegroundRole(value ? QPalette::Highlight : QPalette::WindowText);
});
+ item->setContextMenuPolicy(Qt::ActionsContextMenu);
+ auto removeAction = new QAction(tr("Remove"));
+ item->addAction(removeAction);
+ connect(removeAction, &QAction::triggered, item, [item] {
+ QSettings settings;
+ QStringList keywords = settings.value(recentChannelsKey).toStringList();
+ QString keyword = item->property("keyword").toString();
+ keywords.removeOne(keyword);
+ settings.setValue(recentChannelsKey, keywords);
+ item->deleteLater();
+ });
recentChannelsLayout->addWidget(item);
}
}
void SearchView::onChannelSuggestions(const QVector<Suggestion *> &suggestions) {
lastChannelSuggestions = suggestions;
}
+
+void SearchView::maybeShowMessage() {
+ QSettings settings;
+ QString key;
+
+ bool showMessages = true;
+#ifdef APP_ACTIVATION
+ showMessages = Activation::instance().isActivated();
+#endif
+
+#if defined APP_MAC && !defined APP_MAC_STORE
+ if (showMessages && !settings.contains(key = "sofa")) {
+ QString msg = tr("Need a remote control for %1? Try %2!").arg(Constants::NAME).arg("Sofa");
+ msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + '/' + key +
+ "' style = 'text-decoration:none;color:palette(windowText)' > " + msg + "</a>";
+ messageBar->setMessage(msg);
+ connect(messageBar, &MessageBar::closed, this, [key] {
+ QSettings settings;
+ settings.setValue(key, true);
+ });
+ messageBar->show();
+ showMessages = false;
+ }
+#endif
+
+ if (showMessages) {
+ key = "donate" + QLatin1String(Constants::VERSION);
+ if (!settings.contains(key)) {
+ bool oneYearUsage = true;
+#ifdef APP_ACTIVATION
+ oneYearUsage = (QDateTime::currentSecsSinceEpoch() -
+ Activation::instance().getLicenseTimestamp()) > 86400 * 365;
+#endif
+ if (oneYearUsage) {
+ QString msg =
+ tr("I keep improving %1 to make it the best I can. Support this work!")
+ .arg(Constants::NAME);
+ msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + "/donate" +
+ "' style = 'text-decoration:none;color:palette(windowText)' > " + msg +
+ "</a>";
+ messageBar->setMessage(msg);
+ connect(messageBar, &MessageBar::closed, this, [key] {
+ QSettings settings;
+ settings.setValue(key, true);
+ });
+ messageBar->show();
+ }
+ }
+ }
+}
class ChannelSuggest;
class Suggestion;
class ClickableLabel;
+class MessageBar;
class SearchView : public View {
Q_OBJECT
void onChannelSuggestions(const QVector<Suggestion *> &suggestions);
private:
+ void maybeShowMessage();
YTSuggester *youtubeSuggest;
ChannelSuggest *channelSuggest;
+ MessageBar *messageBar;
SearchWidget *queryEdit;
QLabel *recentKeywordsLabel;
QBoxLayout *recentKeywordsLayout;
QLabel *recentChannelsLabel;
QBoxLayout *recentChannelsLayout;
- QLabel *message;
QStringList recentKeywords;
QStringList recentChannels;
QString Temporary::filename() {
static const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
- QString tempFile = tempDir + "/" + Constants::UNIX_NAME + "-" + QString::number(qrand());
+ QString tempFile = tempDir + "/" + Constants::UNIX_NAME + "-" +
+ QString::number(QRandomGenerator::global()->generate());
#ifdef APP_LINUX
if (userName.isNull()) {
q.addQueryItem("chart", "mostPopular");
q.addQueryItem("maxResults", "1");
url.setQuery(q);
- QObject *reply = HttpUtils::yt().get(url);
+ QObject *reply = HttpUtils::stealthAndNotCached().get(url);
connect(reply, SIGNAL(finished(HttpReply)), SLOT(testResponse(HttpReply)));
}
.arg(videoId, elTypes.at(elIndex)));
}
- QObject *reply = HttpUtils::yt().get(url);
+ QObject *reply = HttpUtils::stealthAndNotCached().get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
connect(reply, SIGNAL(error(QString)), SLOT(emitError(QString)));
QString url = formatObj["url"].toString();
if (url.isEmpty()) {
QString cipher = formatObj["cipher"].toString();
+ if (cipher.isEmpty()) cipher = formatObj["signatureCipher"].toString();
QUrlQuery q(cipher);
qDebug() << "Cipher is " << q.toString();
url = q.queryItemValue("url").trimmed();
*/
if (urlMap.isEmpty()) {
+ qDebug() << "empty urlMap, trying next el";
elIndex++;
getVideoInfo();
return;
q.addQueryItem("bpctr", "9999999999");
url.setQuery(q);
- // QUrl url("https://www.youtube.com/embed/" + videoId);
-
qDebug() << "Loading webpage" << url;
QObject *reply = HttpUtils::yt().get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
// see you in scrapWebPage(QByteArray)
}
+void YTVideo::loadEmbedPage() {
+ QUrl url("https://www.youtube.com/embed/" + videoId);
+ auto reply = HttpUtils::yt().get(url);
+ connect(reply, &HttpReply::finished, this, [this](const HttpReply &reply) {
+ if (!reply.isSuccessful()) {
+ getVideoInfo();
+ return;
+ }
+ static const QRegExp embedRE("\"sts\"\\s*:\\s*(\\d+)");
+ QString sts;
+ if (embedRE.indexIn(reply.body()) == -1) {
+ // qDebug() << "Cannot get sts" << reply.body();
+ } else {
+ sts = embedRE.cap(1);
+ qDebug() << "sts" << sts;
+ }
+ QUrlQuery q;
+ q.addQueryItem("video_id", videoId);
+ q.addQueryItem("eurl", "https://youtube.googleapis.com/v/" + videoId);
+ q.addQueryItem("sts", sts);
+ QUrl url = QUrl("https://www.youtube.com/get_video_info");
+ url.setQuery(q);
+ HttpReply *r = HttpUtils::stealthAndNotCached().get(url);
+ connect(r, &HttpReply::data, this, [this](const QByteArray &bytes) {
+ QByteArray decodedBytes = QByteArray::fromPercentEncoding(bytes);
+ gotVideoInfo(decodedBytes);
+ });
+ connect(r, &HttpReply::error, this, &YTVideo::emitError);
+ });
+}
+
void YTVideo::emitError(const QString &message) {
qWarning() << message;
emit errorStreamUrl(message);
// qDebug() << "scrapeWebPage" << html;
static const QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
- if (ageGateRE.indexIn(html) != -1) {
+ if (ageGateRE.indexIn(html) != -1 || html.contains("desktopLegacyAgeGateReason")) {
qDebug() << "Found ageGate";
ageGate = true;
- elIndex = 4;
- getVideoInfo();
+ // elIndex = 4;
+ // getVideoInfo();
+ loadEmbedPage();
return;
}
}
if (fmtUrlMap.isEmpty() && urlMap.isEmpty()) {
- qWarning() << "Cannot get fmtUrlMap from video page. Trying next el";
+ qDebug() << "Cannot get fmtUrlMap from video page. Trying next el";
// elIndex++;
// getVideoInfo();
// return;
jsPlayerIdRe.indexIn(jsPlayerUrl);
QString jsPlayerId = jsPlayerRe.cap(1);
*/
- QObject *reply = HttpUtils::yt().get(jsPlayerUrl);
+ QObject *reply = HttpUtils::stealthAndNotCached().get(jsPlayerUrl);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
connect(reply, SIGNAL(error(QString)), SLOT(emitError(QString)));
+ } else {
+ qDebug() << "Cannot find jsPlayer";
}
}
break;
}
}
- if (sigFuncName.isEmpty()) qDebug() << "Empty signature function name";
+ if (sigFuncName.isEmpty()) qDebug() << "Empty signature function name" << jsPlayer;
// parseFmtUrlMap(fmtUrlMap, true);
getVideoInfo();
void getVideoInfo();
void parseFmtUrlMap(const QString &fmtUrlMap);
void loadWebPage();
+ void loadEmbedPage();
void captureFunction(const QString &name, const QString &js);
void captureObject(const QString &name, const QString &js);
QString decryptSignature(const QString &s);
SearchView *[recentItem="true"]:focus {
outline: 1px solid palette(highlight);
}
+
+MessageBar {
+ background: #f9f7c8;
+ border-radius: 5px;
+}
+
+MessageBar QLabel {
+ color: rgba(0,0,0,192);
+}
+
+MessageBar QToolButton {
+ background: transparent;
+ border: 0;
+}