]> git.sur5r.net Git - minitube/commitdiff
New upstream version 3.9.1
authorJakob Haufe <sur5r@debian.org>
Wed, 15 Sep 2021 07:25:21 +0000 (07:25 +0000)
committerJakob Haufe <sur5r@debian.org>
Wed, 15 Sep 2021 07:25:21 +0000 (07:25 +0000)
46 files changed:
lib/http/src/http.cpp
lib/http/src/networkhttpreply.cpp
lib/js/js.cpp
lib/js/js.h
lib/js/jsnamfactory.cpp
lib/promises/LICENSE [new file with mode: 0644]
lib/promises/emptypromise.h [new file with mode: 0644]
lib/promises/promise.h [new file with mode: 0644]
lib/promises/promises.pri [new file with mode: 0644]
lib/promises/variantpromise.h [new file with mode: 0644]
lib/updater/src/updater.cpp
locale/fi.ts
locale/ja_JP.ts
locale/ro.ts
minitube.pro
org.tordini.flavio.minitube.metainfo.xml
resources.qrc
src/aggregatevideosource.cpp
src/channelaggregator.cpp
src/jsfunctions.cpp
src/mainwindow.cpp
src/mediaview.cpp
src/playlistitemdelegate.cpp
src/playlistmodel.cpp
src/playlistmodel.h
src/regionsview.cpp
src/searchview.cpp
src/standardfeedsview.cpp
src/subscriptionimportview.cpp
src/video.cpp
src/video.h
src/videosourcewidget.cpp
src/yt/invidious/ivlistparser.cpp
src/yt/yt.pri
src/yt/ytjs/ytjs.pri
src/yt/ytjs/ytjschannel.cpp
src/yt/ytjs/ytjschannelsource.cpp
src/yt/ytjs/ytjssearch.cpp
src/yt/ytjs/ytjssinglevideosource.cpp
src/yt/ytjs/ytjstrending.cpp [new file with mode: 0644]
src/yt/ytjs/ytjstrending.h [new file with mode: 0644]
src/yt/ytthumb.cpp [new file with mode: 0644]
src/yt/ytthumb.h [new file with mode: 0644]
src/yt3listparser.cpp
src/ytregions.cpp
src/ytregions.h

index 5df34a624a88b36b0590905c186a426e92247dfc..9d85e4ff2633e20a488c48fbd3eae7f31bef512e 100644 (file)
@@ -5,7 +5,13 @@
 namespace {
 
 QNetworkAccessManager *networkAccessManager() {
-    static thread_local QNetworkAccessManager *nam = new QNetworkAccessManager();
+    static thread_local QNetworkAccessManager *nam = [] {
+        auto nam = new QNetworkAccessManager();
+#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
+        nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
+#endif
+        return nam;
+    }();
     return nam;
 }
 
index c27d77da597e8cb9e14457a32bb7d53e4e367efb..08b17351c135eb9f9be45659b1d08ecbb0165aeb 100644 (file)
@@ -18,8 +18,13 @@ NetworkHttpReply::NetworkHttpReply(const HttpRequest &req, Http &http)
 }
 
 void NetworkHttpReply::setupReply() {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+    connect(networkReply, &QNetworkReply::errorOccurred, this, &NetworkHttpReply::replyError,
+            Qt::UniqueConnection);
+#else
     connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)),
             SLOT(replyError(QNetworkReply::NetworkError)), Qt::UniqueConnection);
+#endif
     connect(networkReply, SIGNAL(finished()), SLOT(replyFinished()), Qt::UniqueConnection);
     connect(networkReply, SIGNAL(downloadProgress(qint64, qint64)),
             SLOT(downloadProgress(qint64, qint64)), Qt::UniqueConnection);
@@ -54,6 +59,7 @@ void NetworkHttpReply::emitFinished() {
 }
 
 void NetworkHttpReply::replyFinished() {
+#if QT_VERSION < QT_VERSION_CHECK(5, 9, 0)
     QUrl redirection = networkReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
     if (redirection.isValid()) {
         HttpRequest redirectReq;
@@ -71,6 +77,7 @@ void NetworkHttpReply::replyFinished() {
         readTimeoutTimer->start();
         return;
     }
+#endif
 
     if (isSuccessful()) {
         bytes = networkReply->readAll();
index 2390ef5fc290383d895d15cf5bcac8a91a6e0cb6..884006497bac55fd3580d303cb0291faf6e96d24 100644 (file)
@@ -55,6 +55,8 @@ JSResult &JS::callFunction(JSResult *result, const QString &name, const QJSValue
         return *result;
     }
 
+    resetNAM();
+
     auto function = engine->evaluate(name);
     if (!function.isCallable()) {
         qWarning() << function.toString() << " is not callable";
@@ -71,6 +73,24 @@ JSResult &JS::callFunction(JSResult *result, const QString &name, const QJSValue
     return *result;
 }
 
+void JS::resetNAM() {
+    class MyCookieJar : public QNetworkCookieJar {
+        bool insertCookie(const QNetworkCookie &cookie) {
+            if (cookie.name().contains("CONSENT")) {
+                qDebug() << "Fixing CONSENT cookie" << cookie;
+                auto cookie2 = cookie;
+                cookie2.setValue(cookie.value().replace("PENDING", "YES"));
+                return QNetworkCookieJar::insertCookie(cookie2);
+            }
+            return QNetworkCookieJar::insertCookie(cookie);
+        }
+    };
+
+    auto nam = getEngine().networkAccessManager();
+    nam->clearAccessCache();
+    nam->setCookieJar(new MyCookieJar());
+}
+
 void JS::initialize() {
     if (url.isEmpty()) {
         qDebug() << "No js url set";
index 8f0e33ade8a218eda9223e42b8166612f061cb8d..104bc4615a57999b5867a9b002af1712ece43b69 100644 (file)
@@ -18,14 +18,15 @@ public:
     auto getId() const { return id; }
 
     // This should be static but cannot bind static functions to QJSEngine
-    Q_INVOKABLE QJSValue clearTimeout(QJSValue id) {
+    Q_INVOKABLE void clearTimeout(QJSValue id) {
+        // qDebug() << "Clearing timer" << id.toString();
         auto timer = getTimers().take(id.toUInt());
         if (timer) {
             timer->stop();
             timer->deleteLater();
         } else
             qDebug() << "Unknown id" << id.toUInt();
-        return QJSValue();
+        return;
     }
     // This should be static but cannot bind static functions to QJSEngine
     Q_INVOKABLE QJSValue setTimeout(QJSValue callback, QJSValue delayTime, QJSValue args) {
@@ -57,7 +58,7 @@ public:
     }
 
     Q_INVOKABLE JSTimer(QObject *parent = nullptr) : QTimer(parent) {
-        setTimerType(Qt::CoarseTimer);
+        setTimerType(Qt::PreciseTimer);
         setSingleShot(true);
         // avoid 0
         static uint counter = 1;
@@ -87,6 +88,7 @@ public:
     QQmlEngine &getEngine() { return *engine; }
 
     JSResult &callFunction(JSResult *result, const QString &name, const QJSValueList &args);
+    void resetNAM();
 
 signals:
     void initialized();
index 3490916455838122983ec5b2d2d4535439e3ecf6..dd14c4063dfe6949c2f0c5a880ceefa7a0bead5b 100644 (file)
@@ -62,8 +62,25 @@ QNetworkReply *JSNAM::createRequest(QNetworkAccessManager::Operation op,
                      << req2.rawHeader(i.key());
     }
 
-    qDebug() << req2.url() << req2.rawHeaderList();
-    return QNetworkAccessManager::createRequest(op, req2, outgoingData);
+#ifndef QT_NO_DEBUG_OUTPUT
+    qDebug() << req2.url();
+    for (const auto &h : req2.rawHeaderList())
+        qDebug() << h << req2.rawHeader(h);
+#endif
+
+    auto reply = QNetworkAccessManager::createRequest(op, req2, outgoingData);
+
+#ifndef QT_NO_DEBUG_OUTPUT
+    connect(reply, &QNetworkReply::finished, this, [reply] {
+        qDebug() << "finished"
+                 << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()
+                 << reply->url() << reply->rawHeaderPairs();
+    });
+    connect(reply, &QNetworkReply::redirectAllowed, this,
+            [reply] { qDebug() << "redirectAllowed" << reply->url(); });
+#endif
+
+    return reply;
 }
 
 QNetworkAccessManager *JSNAMFactory::create(QObject *parent) {
diff --git a/lib/promises/LICENSE b/lib/promises/LICENSE
new file mode 100644 (file)
index 0000000..e3fd3da
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Flavio Tordini
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/promises/emptypromise.h b/lib/promises/emptypromise.h
new file mode 100644 (file)
index 0000000..4b385f0
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef EMPTYPROMISE_H
+#define EMPTYPROMISE_H
+
+#include <QtCore>
+
+class EmptyPromise : public QObject {
+    Q_OBJECT
+
+public:
+    explicit EmptyPromise(QObject *parent = nullptr) : QObject(parent) {
+        connect(this, &EmptyPromise::resolve, this, &QObject::deleteLater);
+        connect(this, &EmptyPromise::reject, this, &QObject::deleteLater);
+    };
+
+    template <typename Functor> EmptyPromise &then(Functor func) {
+        connect(this, &EmptyPromise::resolve, this, func);
+        return *this;
+    }
+    template <typename Functor> EmptyPromise &onFailed(Functor func) {
+        connect(this, &EmptyPromise::reject, this, func);
+        return *this;
+    }
+    template <typename Functor> EmptyPromise &finally(Functor func) {
+        connect(this, &EmptyPromise::destroyed, this, func);
+        return *this;
+    }
+
+signals:
+    void resolve();
+    void reject(const QString &message);
+};
+
+#endif // EMPTYPROMISE_H
diff --git a/lib/promises/promise.h b/lib/promises/promise.h
new file mode 100644 (file)
index 0000000..5bf1c87
--- /dev/null
@@ -0,0 +1,46 @@
+#ifndef PROMISE_H
+#define PROMISE_H
+
+#include <QtCore>
+
+/// private, don't use directly
+class BasePromise : public QObject {
+    Q_OBJECT
+
+public:
+    explicit BasePromise(QObject *parent = nullptr) : QObject(parent) {
+        connect(this, &BasePromise::resolve, this, &QObject::deleteLater);
+        connect(this, &BasePromise::reject, this, &QObject::deleteLater);
+    };
+
+    template <typename Function> BasePromise &then(Function func) {
+        connect(this, &BasePromise::resolve, this, func);
+        return *this;
+    }
+    template <typename Function> BasePromise &onFailed(Function func) {
+        connect(this, &BasePromise::reject, this, func);
+        return *this;
+    }
+    template <typename Function> BasePromise &finally(Function func) {
+        connect(this, &BasePromise::destroyed, this, func);
+        return *this;
+    }
+
+signals:
+    void resolve();
+    void reject(const QString &message);
+};
+
+template <class T> class Promise : public BasePromise {
+public:
+    void resolve(T value) {
+        data = value;
+        resolve();
+    }
+    T result() const { return data; }
+
+private:
+    T data;
+};
+
+#endif // PROMISE_H
diff --git a/lib/promises/promises.pri b/lib/promises/promises.pri
new file mode 100644 (file)
index 0000000..aee3f98
--- /dev/null
@@ -0,0 +1,9 @@
+INCLUDEPATH += $$PWD
+DEPENDPATH += $$PWD
+
+QT *= core
+
+HEADERS += \
+    $$PWD/emptypromise.h \
+    $$PWD/promise.h \
+    $$PWD/variantpromise.h
diff --git a/lib/promises/variantpromise.h b/lib/promises/variantpromise.h
new file mode 100644 (file)
index 0000000..6cc3555
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef VARIANTPROMISE_H
+#define VARIANTPROMISE_H
+
+#include <QtCore>
+
+class VariantPromise : public QObject {
+    Q_OBJECT
+
+public:
+    explicit VariantPromise(QObject *parent = nullptr) : QObject(parent) {
+        connect(this, &VariantPromise::resolve, this, &QObject::deleteLater);
+        connect(this, &VariantPromise::reject, this, &QObject::deleteLater);
+    };
+
+    template <typename Function> VariantPromise &then(Function func) {
+        connect(this, &VariantPromise::resolve, this, func);
+        return *this;
+    }
+    template <typename Function> VariantPromise &onFailed(Function func) {
+        connect(this, &VariantPromise::reject, this, func);
+        return *this;
+    }
+    template <typename Function> VariantPromise &finally(Function func) {
+        connect(this, &VariantPromise::destroyed, this, func);
+        return *this;
+    }
+
+signals:
+    void resolve(QVariant result);
+    void reject(const QString &message);
+};
+
+#endif // VARIANTPROMISE_H
index 3efa41c9fe16392868f0eb920a6fce337b6f26ac..564f2d2f72f44f55daa71b19559783ff5039ee66 100644 (file)
@@ -109,7 +109,7 @@ QLabel *Updater::getLabel() {
             }
             label->setText(t);
         };
-        connect(this, &Updater::statusChanged, this, onStatusChange);
+        connect(this, &Updater::statusChanged, label, onStatusChange);
         onStatusChange(status);
     }
     return label;
index 7c204af1f11804a7ab7e333de243a7afaca99d3e..af54212a63fba452d07d4a542d800ec4374f3806 100644 (file)
     </message>
     <message numerus="yes">
         <source>%n year(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>%n vuotta sitten</numerusform><numerusform>%n vuotta sitten</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>This year</source>
-        <translation type="unfinished"/>
+        <translation>Tänä vuonna</translation>
     </message>
     <message>
         <source>HD</source>
-        <translation type="unfinished"/>
+        <translation>HD</translation>
     </message>
     <message>
         <source>4K</source>
-        <translation type="unfinished"/>
+        <translation>4K</translation>
     </message>
     <message>
         <source>HDR</source>
-        <translation type="unfinished"/>
+        <translation>HDR</translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>An update is ready to be installed. Quit and install update.</source>
-        <translation type="unfinished"/>
+        <translation>Päivitys on valmiina asennettavaksi. Poistu sovelluksesta asentaaksesi päivityksen.</translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>Trending</source>
-        <translation type="unfinished"/>
+        <translation>Trendaavat</translation>
     </message>
     <message>
         <source>Music</source>
-        <translation type="unfinished"/>
+        <translation>Musiikki</translation>
     </message>
     <message>
         <source>News</source>
-        <translation type="unfinished"/>
+        <translation>Uutiset</translation>
     </message>
     <message>
         <source>Movies</source>
-        <translation type="unfinished"/>
+        <translation>Elokuvat</translation>
     </message>
     <message>
         <source>Gaming</source>
-        <translation type="unfinished"/>
+        <translation>Pelaaminen</translation>
     </message>
 </context>
 <context>
     <name>Updater</name>
     <message>
         <source>Check for Updates...</source>
-        <translation type="unfinished"/>
+        <translation>Tarkista onko päivityksiä saatavilla...</translation>
     </message>
     <message>
         <source>Version %1 is available...</source>
-        <translation type="unfinished"/>
+        <translation>Versio %1 on saatavilla...</translation>
     </message>
     <message>
         <source>Downloading version %1...</source>
-        <translation type="unfinished"/>
+        <translation>Ladataan versiota %1...</translation>
     </message>
     <message>
         <source>Restart to Update</source>
-        <translation type="unfinished"/>
+        <translation>Uudelleenkäynnistä päivittääksesi</translation>
     </message>
     <message>
         <source>Version %1 download failed</source>
-        <translation type="unfinished"/>
+        <translation>Version %1 lataaminen epäonnistui</translation>
     </message>
     <message>
         <source>Check for Updates</source>
-        <translation type="unfinished"/>
+        <translation>Tarkista onko päivityksiä saatavilla</translation>
     </message>
     <message>
         <source>Download Update</source>
-        <translation type="unfinished"/>
+        <translation>Lataa päivitys</translation>
     </message>
     <message>
         <source>Downloading update...</source>
     </message>
     <message>
         <source>Retry Update Download</source>
-        <translation type="unfinished"/>
+        <translation>Yritä ladata päivitystä uudelleen</translation>
     </message>
     <message>
         <source>You have the latest version.</source>
-        <translation type="unfinished"/>
+        <translation>Käytössä on jo viimeisin versio.</translation>
     </message>
     <message>
         <source>Version %1 is available.</source>
-        <translation type="unfinished"/>
+        <translation>Versio %1 on saatavissa.</translation>
     </message>
     <message>
         <source>An update has been downloaded and is ready to be installed.</source>
-        <translation type="unfinished"/>
+        <translation>Päivitys ladattiin ja on nyt valmis asennettavaksi.</translation>
     </message>
 </context>
 <context>
     <name>updater::DefaultUpdater</name>
     <message>
         <source>There are currently no updates available.</source>
-        <translation type="unfinished"/>
+        <translation>Päivityksiä ei ole tällä hetkellä saatavilla.</translation>
     </message>
 </context>
 <context>
     <name>updater::Dialog</name>
     <message>
         <source>You already have the latest version</source>
-        <translation type="unfinished"/>
+        <translation>Käytössä on jo viimeisin versio</translation>
     </message>
     <message>
         <source>Downloading %1 %2...</source>
-        <translation type="unfinished"/>
+        <translation>Ladataan %1 %2...</translation>
     </message>
     <message>
         <source>A new version of %1 is available!</source>
     </message>
     <message>
         <source>Download Update</source>
-        <translation type="unfinished"/>
+        <translation>Lataa päivitys</translation>
     </message>
 </context>
 </TS>
\ No newline at end of file
index c75e281cce7d8ba1e24623d8f1723ce63667003d..7ada89c434243b78a051271263a39178f6f8f47d 100644 (file)
     </message>
     <message numerus="yes">
         <source>%n year(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n年前</numerusform></translation>
     </message>
 </context>
 <context>
index 389af36f9c8eeb427300aeef50720f44050c5689..2f9cc3d5b4fe60673c3c4749440d3d7c7e6359d3 100644 (file)
     </message>
     <message>
         <source>Powered by %1</source>
-        <translation type="unfinished"/>
+        <translation>Alimentat de %1</translation>
     </message>
     <message>
         <source>Open-source software</source>
-        <translation type="unfinished"/>
+        <translation>Software Sursă-Deschisă</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><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>Aveți un videoclip nou</numerusform><numerusform>Aveți %n videoclipuri noi</numerusform><numerusform>Aveți %n de videoclipuri noi</numerusform></translation>
     </message>
 </context>
 <context>
index e48dbb2da2ea2a123dbffefde4dd276bcc3f1492..71420f3121d6789d22d86592ebafefad0bea293e 100644 (file)
@@ -1,7 +1,7 @@
 CONFIG += c++17 exceptions_off rtti_off object_parallel_to_source
 
 TEMPLATE = app
-VERSION = 3.8.1
+VERSION = 3.9.1
 DEFINES += APP_VERSION="$$VERSION"
 
 APP_NAME = Minitube
@@ -38,6 +38,7 @@ QT += widgets network sql qml
 include(lib/http/http.pri)
 include(lib/idle/idle.pri)
 include(lib/js/js.pri)
+include(lib/promises/promises.pri)
 
 DEFINES += MEDIA_MPV
 include(lib/media/media.pri)
index 9815df0c7aaa485a8c4088660170324650ff57c7..884b05c3ecee6839e3ea4a9d07fdddbfce8ad00c 100644 (file)
@@ -2,6 +2,7 @@
 <component type="desktop-application">
   <name>Minitube</name>
   <id>org.tordini.flavio.minitube</id>
+  <launchable type="desktop-id">minitube.desktop</launchable>
   <metadata_license>CC0-1.0</metadata_license>
   <project_license>GPL-3.0-or-later</project_license>
   <summary>YouTube app</summary>
   <url type="donation">https://flavio.tordini.org/donate</url>
   <url type="translate">https://www.transifex.com/flaviotordini/minitube/</url>
   <releases>
+    <release version="3.9" date="2021-06-22"/>
+    <release version="3.8.2" date="2021-04-02"/>
+    <release version="3.8.1" date="2021-02-26"/>
+    <release version="3.8" date="2021-02-19"/>
     <release version="3.7" date="2020-12-30"/>
   </releases>
   <content_rating type="oars-1.1">
index 9933d32159a1412fa4a66f35af708eace8c5f22b..c229b2685e6fcb62024dd7b7ca2e6a99aa0aeb2e 100644 (file)
@@ -52,5 +52,6 @@
         <file>images/app@2x.png</file>
         <file>images/64x64/app.png</file>
         <file>images/64x64/app@2x.png</file>
+        <file>flags/us.png</file>
     </qresource>
 </RCC>
index 95300f8a4e54d092c3d526d36dc2d6cd7dd3ce28..cf9b6a89801c313592314c23c7597aa54d398611 100644 (file)
@@ -63,7 +63,18 @@ void AggregateVideoSource::loadVideos(int max, int startIndex) {
         video->setChannelId(query.value(4).toString());
         video->setDescription(query.value(5).toString());
         video->setWebpage(query.value(6).toString());
-        video->setThumbnailUrl(query.value(7).toString());
+
+        QString thumbString = query.value(7).toString();
+        if (thumbString.startsWith('[')) {
+            const auto thumbs = QJsonDocument::fromJson(thumbString.toUtf8()).array();
+            for (const auto &t : thumbs) {
+                video->addThumb(t["width"].toInt(), t["height"].toInt(), t["url"].toString());
+            }
+        } else {
+            // assume it's a URL
+            video->addThumb(0, 0, thumbString);
+        }
+
         video->setViewCount(query.value(8).toInt());
         video->setDuration(query.value(9).toInt());
         videos << video;
index d13779202f50cf714ca63689e96ec0c053931ab1..8097af180e51fe03d44ed32383b50b61cc8e42f9 100644 (file)
@@ -132,7 +132,7 @@ void ChannelAggregator::parseWebPage(const QByteArray &bytes) {
     } else {
         currentChannel->updateChecked();
         currentChannel = 0;
-        processNextChannel();
+        QTimer::singleShot(5000, this, &ChannelAggregator::processNextChannel);
     }
 }
 
@@ -273,7 +273,18 @@ void ChannelAggregator::addVideo(Video *video) {
     query.bindValue(7, video->getChannelId());
     query.bindValue(8, video->getDescription());
     query.bindValue(9, video->getWebpage());
-    query.bindValue(10, video->getThumbnailUrl());
+
+    QJsonDocument thumbsDoc;
+    auto thumbsArray = thumbsDoc.array();
+    for (const auto &t : video->getThumbs()) {
+        thumbsArray.append(QJsonObject{
+                {"url", t.getUrl()},
+                {"width", t.getWidth()},
+                {"height", t.getHeight()},
+        });
+    }
+    thumbsDoc.setArray(thumbsArray);
+    query.bindValue(10, thumbsDoc.toJson(QJsonDocument::Compact));
     query.bindValue(11, video->getViewCount());
     query.bindValue(12, video->getDuration());
     success = query.exec();
index 679d1e3df3306ee1ab27831905ac58b3df4c5bd3..96f221fe926e2799ed6aad76bfeac10e08dcfc35 100644 (file)
@@ -57,7 +57,10 @@ void JsFunctions::parseJs(const QString &js) {
     if (engine) delete engine;
     engine = new QJSEngine(this);
     engine->evaluate(js);
-    emit ready();
+    QTimer::singleShot(0, this, [this] {
+        qDebug() << "Emitting ready";
+        emit ready();
+    });
 }
 
 QString JsFunctions::jsFilename() {
index 8b001ac0864d24e98fc67ddce7d113973c82eaa2..610c0d16b680c69c78e49d0e0fd8977289293a77 100644 (file)
@@ -171,13 +171,16 @@ MainWindow::MainWindow()
     } else if (VideoAPI::impl() == VideoAPI::YT3) {
         YT3::instance().initApiKeys();
     } else if (VideoAPI::impl() == VideoAPI::JS) {
-        JS::instance().getNamFactory().setRequestHeaders(
-                {{"User-Agent", HttpUtils::stealthUserAgent()}});
         JS::instance().initialize(QUrl(QLatin1String(Constants::WEBSITE) + "-ws/bundle2.js"));
-        /// JS::instance().initialize(QUrl("http://localhost:8000/bundle-test.js"));
+        // JS::instance().initialize(QUrl("http://localhost:8000/bundle-test.js"));
         Invidious::instance().initServers();
     }
 
+    connect(JsFunctions::instance(), &JsFunctions::ready, this, [] {
+        auto ua = JsFunctions::instance()->string("userAgent()").toUtf8();
+        JS::instance().getNamFactory().setRequestHeaders({{"User-Agent", ua}});
+    });
+
     QTimer::singleShot(100, this, &MainWindow::lazyInit);
 }
 
@@ -226,8 +229,6 @@ void MainWindow::lazyInit() {
     fullscreenTimer->setSingleShot(true);
     connect(fullscreenTimer, SIGNAL(timeout()), SLOT(hideFullscreenUI()));
 
-    JsFunctions::instance();
-
     // Hack to give focus to searchlineedit
     View *view = qobject_cast<View *>(views->currentWidget());
     if (view == homeView) {
@@ -648,7 +649,7 @@ void MainWindow::createActions() {
     action->setEnabled(false);
     actionMap.insert("refineSearch", action);
 
-    action = new QAction(YTRegions::worldwideRegion().name, this);
+    action = new QAction(YTRegions::defaultRegion().name, this);
     actionMap.insert("worldwideRegion", action);
 
     action = new QAction(YTRegions::localRegion().name, this);
index e0aebfd8d8b47ec4b66d1a53bbe1e87eecaa3fba..5d74d0ba379ec3f2fa290fb7b52521c623a47890 100644 (file)
@@ -254,6 +254,7 @@ void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool
                 VideoSource *vs = history.takeLast();
                 if (!vs->parent()) {
                     qDebug() << "Deleting VideoSource" << vs->getName() << vs;
+                    vs->abort();
                     vs->deleteLater();
                 }
             }
@@ -376,20 +377,25 @@ void MediaView::mediaStateChanged(Media::State state) {
 void MediaView::pause() {
     switch (media->state()) {
     case Media::PlayingState:
+        qDebug() << "Pausing";
         media->pause();
         pauseTimer.start();
         break;
     default:
-        if (pauseTimer.hasExpired(60000)) {
+        if (pauseTimer.isValid() && pauseTimer.hasExpired(60000)) {
+            qDebug() << "Pause timer expired";
             pauseTimer.invalidate();
             auto activeVideo = playlistModel->activeVideo();
             if (activeVideo) {
                 connect(activeVideo, &Video::gotStreamUrl, this,
                         &MediaView::resumeWithNewStreamUrl);
                 activeVideo->loadStreamUrl();
-            }
-        } else
+            } else
+                qDebug() << "No active video";
+        } else {
+            qDebug() << "Playing" << media->file();
             media->play();
+        }
         break;
     }
 }
@@ -405,6 +411,7 @@ void MediaView::stop() {
         VideoSource *videoSource = history.takeFirst();
         // Don't delete videoSource in the Browse view
         if (!videoSource->parent()) {
+            videoSource->abort();
             videoSource->deleteLater();
         }
     }
index 378bba33d3293b49b99d9116c1ffa0dfc7c2c67e..33d431e9bb9e594ffcd978e38a019012205c682e 100644 (file)
@@ -25,6 +25,7 @@ $END_LICENSE */
 #include "iconutils.h"
 #include "playlistmodel.h"
 #include "playlistview.h"
+#include "variantpromise.h"
 #include "video.h"
 #include "videodefinition.h"
 
@@ -128,17 +129,31 @@ void PlaylistItemDelegate::paintBody(QPainter *painter,
     const bool isActive = index.data(ActiveTrackRole).toBool();
 
     // get the video metadata
-    const Video *video = index.data(VideoRole).value<VideoPointer>().data();
+    Video *video = index.data(VideoRole).value<VideoPointer>().data();
 
     // draw the "current track" highlight underneath the text
     if (isActive && !isSelected) paintActiveOverlay(painter, option, line);
 
     // thumb
-    const QPixmap &thumb = video->getThumbnail();
+    qreal pixelRatio = painter->device()->devicePixelRatioF();
+    QByteArray thumbKey = ("t" + QString::number(pixelRatio)).toUtf8();
+    const QPixmap &thumb = video->property(thumbKey).value<QPixmap>();
     if (!thumb.isNull()) {
         painter->drawPixmap(0, 0, thumb);
         if (video->getDuration() > 0) drawTime(painter, video->getFormattedDuration(), line);
-    }
+    } else
+        video->loadThumb({thumbWidth, thumbHeight}, pixelRatio)
+                .then([pixelRatio, thumbKey, video](auto variant) {
+                    QPixmap pixmap;
+                    pixmap.loadFromData(variant.toByteArray());
+                    pixmap.setDevicePixelRatio(pixelRatio);
+                    const int thumbWidth = PlaylistItemDelegate::thumbWidth * pixelRatio;
+                    if (pixmap.width() > thumbWidth)
+                        pixmap = pixmap.scaledToWidth(thumbWidth, Qt::SmoothTransformation);
+                    video->setProperty(thumbKey, pixmap);
+                    video->changed();
+                })
+                .onFailed([](auto msg) { qDebug() << msg; });
 
     const bool thumbsOnly = line.width() <= thumbWidth + 60;
     const bool isHovered = index.data(HoveredItemRole).toBool();
index be0819479e1a5096b6bfe58d7c05bc218d0fc903..f2e0e5f32d0f5c9a10c4c6e84e3dfa85e7c311e8 100644 (file)
@@ -20,6 +20,7 @@ $END_LICENSE */
 
 #include "playlistmodel.h"
 #include "mediaview.h"
+#include "playlistitemdelegate.h"
 #include "searchparams.h"
 #include "video.h"
 #include "videomimedata.h"
@@ -242,8 +243,10 @@ void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
     videos.append(newVideos);
     endInsertRows();
     for (Video *video : newVideos) {
-        connect(video, SIGNAL(gotThumbnail()), SLOT(updateVideoSender()), Qt::UniqueConnection);
-        video->loadThumbnail();
+        connect(video, &Video::changed, this, [video, this] {
+            int row = rowForVideo(video);
+            emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
+        });
     }
 }
 
@@ -297,16 +300,6 @@ void PlaylistModel::handleFirstVideo(Video *video) {
     }
 }
 
-void PlaylistModel::updateVideoSender() {
-    Video *video = static_cast<Video *>(sender());
-    if (!video) {
-        qDebug() << "Cannot get sender";
-        return;
-    }
-    int row = rowForVideo(video);
-    emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
-}
-
 void PlaylistModel::emitDataChanged() {
     QModelIndex index = createIndex(rowCount() - 1, 0);
     emit dataChanged(index, index);
index 2cba04a89a55b1954d559541763f5942d98ba0a3..bd0fd2df6fc007f020aebbb8185ba688294b24ec 100644 (file)
@@ -89,7 +89,6 @@ public slots:
     void addVideos(const QVector<Video *> &newVideos);
     void searchFinished(int total);
     void searchError(const QString &message);
-    void updateVideoSender();
     void emitDataChanged();
 
     void setHoveredRow(int row);
index b5c33430eb0a83e9a151aee32cc8e16cd9402d7a..6694b88c2de40c876024b752fdd040e547ed1351 100644 (file)
@@ -32,7 +32,7 @@ RegionsView::RegionsView(QWidget *parent) : View(parent) {
     layout->setSpacing(0);
     l->addLayout(layout);
 
-    addRegion(YTRegions::worldwideRegion());
+    addRegion(YTRegions::defaultRegion());
     foreach (YTRegion region, YTRegions::list())
         addRegion(region);
 
index 5315ca3e32632b9cef5b8531cac699d69678485d..e92d97d9ebf2ca57c7c00b9c283918c9e71d1898 100644 (file)
@@ -488,7 +488,7 @@ void SearchView::maybeShowMessage() {
 #ifdef APP_ACTIVATION
             oneYearUsage = (QDateTime::currentSecsSinceEpoch() -
                             Activation::instance().getLicenseTimestamp()) > 86400 * 365;
-#elif APP_MAC_STORE
+#elif defined APP_MAC_STORE
             oneYearUsage = false;
 #endif
             if (oneYearUsage) {
index 464f6d06330398e82e6680a7cb1be54ea4abd251..2fb697d9bbe3c72e044324bbeb455b348c9845cf 100644 (file)
@@ -29,6 +29,8 @@ $END_LICENSE */
 #include "ivvideolist.h"
 #include "videoapi.h"
 
+#include "ytjstrending.h"
+
 StandardFeedsView::StandardFeedsView(QWidget *parent) : View(parent), layout(0) {
     setBackgroundRole(QPalette::Base);
     setAutoFillBackground(true);
@@ -46,12 +48,26 @@ void StandardFeedsView::load() {
 
     YTRegion region = YTRegions::currentRegion();
 
+    // TODO consolidate in YT
     if (VideoAPI::impl() == VideoAPI::YT3) {
         YTCategories *youTubeCategories = new YTCategories(this);
         connect(youTubeCategories, SIGNAL(categoriesLoaded(const QVector<YTCategory> &)),
                 SLOT(layoutCategories(const QVector<YTCategory> &)));
         youTubeCategories->loadCategories();
         addVideoSourceWidget(buildStandardFeed("most_popular", tr("Most Popular")));
+    } else if (VideoAPI::impl() == VideoAPI::JS) {
+        const QMap<QString, QString> pages = {{"default", tr("Trending")},
+                                              {"music", tr("Music")},
+                                              {"movies", tr("Movies")},
+                                              {"gaming", tr("Gaming")}};
+        auto i = pages.constBegin();
+        while (i != pages.constEnd()) {
+            addVideoSourceWidget(
+                    new YTJSTrending(i.value(), {{"page", i.key()}, {"geoLocation", region.id}}));
+            ++i;
+        }
+
+        setUpdatesEnabled(true);
     } else {
         QString regionParam = "region=" + region.id;
         addVideoSourceWidget(new IVVideoList("popular?" + regionParam, tr("Most Popular")));
@@ -88,7 +104,7 @@ void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
     connect(w, SIGNAL(unavailable(VideoSourceWidget *)),
             SLOT(removeVideoSourceWidget(VideoSourceWidget *)));
     int i = layout->count();
-    const int cols = VideoAPI::impl() == VideoAPI::YT3 ? 5 : 3;
+    const int cols = VideoAPI::impl() == VideoAPI::YT3 ? 5 : 2;
     layout->addWidget(w, i / cols, i % cols);
 }
 
@@ -107,7 +123,7 @@ void StandardFeedsView::removeVideoSourceWidget(VideoSourceWidget *videoSourceWi
     }
 
     const int itemCount = items.size();
-    const int cols = 4; // itemCount / 3;
+    const int cols = 2; // itemCount / 3;
     for (int i = itemCount - 1; i >= 0; i--) {
         QLayoutItem *item = items.at(i);
         int index = itemCount - 1 - i;
@@ -155,7 +171,7 @@ void StandardFeedsView::disappear() {
 }
 
 void StandardFeedsView::selectWorldwideRegion() {
-    YTRegions::setRegion(YTRegions::worldwideRegion().id);
+    YTRegions::setRegion(YTRegions::defaultRegion().id);
     load();
 }
 
index a1a414d133991c6955426cca289902c5ab2b9c95..8283b4f8228a0fdbf64f590fa62e8896daa35042 100644 (file)
@@ -46,26 +46,40 @@ SubscriptionImportView::SubscriptionImportView(QWidget *parent) : View(parent) {
     tip->setFont(FontUtils::medium());
     layout->addWidget(tip);
 
-    auto button = new QPushButton("Open subscriptions.json");
+    auto button = new QPushButton("Open subscriptions.csv");
     button->setDefault(true);
     connect(this, &View::didAppear, button, [button] { button->setFocus(); });
     button->setFocus();
     button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
     connect(button, &QPushButton::clicked, this, [this] {
         auto dir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
-        QString fileName = QFileDialog::getOpenFileName(this, tr("Open subscriptions.json"), dir,
-                                                        tr("JSON Files (*.json)"));
+        QString fileName = QFileDialog::getOpenFileName(this, tr("Open subscriptions.csv"), dir,
+                                                        tr("YouTube data (*.csv *.json)"));
         if (!fileName.isEmpty()) {
             auto w = MainWindow::instance();
             QString msg;
             QFile file(fileName);
             if (file.open(QFile::ReadOnly)) {
                 int count = 0;
-                const auto array = QJsonDocument::fromJson(file.readAll()).array();
-                for (const auto &i : array) {
-                    auto id = i["snippet"]["resourceId"]["channelId"].toString();
-                    qDebug() << "Subscribing to" << id;
-                    if (YTChannel::subscribe(id)) count++;
+                if (QFileInfo(fileName).suffix().toLower() == "csv") {
+                    int lineNumber = 1;
+                    while (!file.atEnd()) {
+                        QByteArray line = file.readLine();
+                        if (lineNumber > 1) {
+                            auto fields = line.split(',');
+                            auto id = fields.first();
+                            qDebug() << "Subscribing to" << id;
+                            if (YTChannel::subscribe(id)) count++;
+                        }
+                        lineNumber++;
+                    }
+                } else {
+                    const auto array = QJsonDocument::fromJson(file.readAll()).array();
+                    for (const auto &i : array) {
+                        auto id = i["snippet"]["resourceId"]["channelId"].toString();
+                        qDebug() << "Subscribing to" << id;
+                        if (YTChannel::subscribe(id)) count++;
+                    }
                 }
                 msg = tr("Subscribed to %n channel(s)", "", count);
                 w->showHome();
index fdf7a22f11ce09e681f99b36666c5dc3547ae93a..3a79f7ac272acf9420a71503e1fe74fca6558959 100644 (file)
@@ -29,9 +29,11 @@ $END_LICENSE */
 #include "ytjsvideo.h"
 #include "ytvideo.h"
 
+#include "variantpromise.h"
+
 Video::Video()
-    : duration(0), viewCount(-1), license(LicenseYouTube), definitionCode(0),
-      loadingThumbnail(false), ytVideo(nullptr), ytjsVideo(nullptr) {}
+    : duration(0), viewCount(-1), license(LicenseYouTube), definitionCode(0), ytVideo(nullptr),
+      ytjsVideo(nullptr) {}
 
 Video::~Video() {
     qDebug() << "Deleting" << id;
@@ -45,9 +47,8 @@ Video *Video::clone() {
     clone->channelId = channelId;
     clone->webpage = webpage;
     clone->streamUrl = streamUrl;
-    clone->thumbnail = thumbnail;
-    clone->thumbnailUrl = thumbnailUrl;
-    clone->mediumThumbnailUrl = mediumThumbnailUrl;
+    clone->thumbs = thumbs;
+    clone->thumbsNeedSorting = thumbsNeedSorting;
     clone->duration = duration;
     clone->formattedDuration = formattedDuration;
     clone->published = published;
@@ -81,17 +82,6 @@ void Video::setWebpage(const QString &value) {
     }
 }
 
-void Video::loadThumbnail() {
-    if (thumbnailUrl.isEmpty() || loadingThumbnail) return;
-    loadingThumbnail = true;
-    auto reply = HttpUtils::yt().get(thumbnailUrl);
-    connect(reply, SIGNAL(data(QByteArray)), SLOT(setThumbnail(QByteArray)));
-    connect(reply, &HttpReply::error, this, [this](auto &msg) {
-        qWarning() << msg;
-        loadingThumbnail = false;
-    });
-}
-
 void Video::setDuration(int value) {
     duration = value;
     formattedDuration = DataUtils::formatDuration(duration);
@@ -107,17 +97,6 @@ void Video::setPublished(const QDateTime &value) {
     formattedPublished = DataUtils::formatDateTime(published);
 }
 
-void Video::setThumbnail(const QByteArray &bytes) {
-    qreal ratio = qApp->devicePixelRatio();
-    thumbnail.loadFromData(bytes);
-    thumbnail.setDevicePixelRatio(ratio);
-    const int thumbWidth = PlaylistItemDelegate::thumbWidth * ratio;
-    if (thumbnail.width() > thumbWidth)
-        thumbnail = thumbnail.scaledToWidth(thumbWidth, Qt::SmoothTransformation);
-    emit gotThumbnail();
-    loadingThumbnail = false;
-}
-
 void Video::streamUrlLoaded(const QString &streamUrl, const QString &audioUrl) {
     qDebug() << "Streams loaded";
     this->streamUrl = streamUrl;
@@ -145,7 +124,8 @@ void Video::loadStreamUrlJS() {
         qDebug() << msg;
         ytjsVideo->deleteLater();
         ytjsVideo = nullptr;
-        loadStreamUrlYT();
+        // loadStreamUrlYT();
+        emit errorStreamUrl(msg);        
     });
     ytjsVideo->loadStreamUrl();
 }
@@ -170,10 +150,85 @@ void Video::loadStreamUrl() {
     loadStreamUrlJS();
 }
 
+bool Video::isLoadingStreamUrl() const {
+    return ytVideo != nullptr || ytjsVideo != nullptr;
+}
+
 void Video::abortLoadStreamUrl() {
     if (ytVideo) {
         ytVideo->disconnect(this);
         ytVideo->deleteLater();
         ytVideo = nullptr;
     }
+    if (ytjsVideo) {
+        ytjsVideo->disconnect(this);
+        ytjsVideo->deleteLater();
+        ytjsVideo = nullptr;
+    }
+}
+
+void Video::addThumb(int width, int height, QString url) {
+    thumbs << YTThumb(width, height, url);
+    thumbsNeedSorting = true;
+}
+
+VariantPromise &Video::loadThumb(QSize size, qreal pixelRatio) {
+    if (thumbsNeedSorting) {
+        std::sort(thumbs.begin(), thumbs.end(),
+                  [](auto a, auto b) { return a.getWidth() < b.getWidth(); });
+        thumbsNeedSorting = false;
+    }
+
+    auto promise = new VariantPromise(this);
+    if (thumbs.isEmpty()) {
+        QTimer::singleShot(0, promise, [promise] { promise->reject("Empty thumbs"); });
+        return *promise;
+    }
+
+    auto reallyLoad = [this, promise, size, pixelRatio](auto &&self,
+                                                        YTThumb *previous = nullptr) -> void {
+        YTThumb *selected = nullptr;
+
+        static bool fallback = false;
+        if (fallback) {
+            qDebug() << "Doing fallback loop";
+            bool skip = previous != nullptr;
+            for (int i = thumbs.size() - 1; i >= 0; --i) {
+                auto &thumb = thumbs.at(i);
+                if (!skip) {
+                    selected = (YTThumb *)&thumb;
+                    qDebug() << "selected" << selected->getUrl();
+                    break;
+                }
+                if (&thumb == previous) skip = false;
+            }
+        } else {
+            bool skip = previous != nullptr;
+            for (auto &thumb : qAsConst(thumbs)) {
+                if (!skip && thumb.getWidth() * pixelRatio >= size.width() &&
+                    thumb.getHeight() * pixelRatio >= size.height()) {
+                    selected = (YTThumb *)&thumb;
+                    qDebug() << "selected" << selected->getUrl();
+                    break;
+                }
+                if (&thumb == previous) skip = false;
+            }
+        }
+        if (!selected && !fallback) {
+            qDebug() << "Falling back";
+            fallback = true;
+            self(self);
+            return;
+        }
+        if (selected) {
+            qDebug() << "Loading" << selected->getUrl();
+            selected->load(promise)
+                    .then([promise](auto variant) { promise->resolve(variant); })
+                    .onFailed([self, selected] { self(self, selected); });
+        } else
+            promise->reject("No thumb");
+    };
+    reallyLoad(reallyLoad);
+
+    return *promise;
 }
index f94186e5c3f09684f2af9160211d00c5e0fa8dcf..0aa4443aad86371e5b1db84219ef9ee3f590b60b 100644 (file)
@@ -24,8 +24,11 @@ $END_LICENSE */
 #include <QtCore>
 #include <QtGui>
 
+#include "ytthumb.h"
+
 class YTVideo;
 class YTJSVideo;
+class VariantPromise;
 
 class Video : public QObject {
     Q_OBJECT
@@ -53,18 +56,6 @@ public:
     const QString &getWebpage();
     void setWebpage(const QString &value);
 
-    void loadThumbnail();
-    const QPixmap &getThumbnail() const { return thumbnail; }
-
-    const QString &getThumbnailUrl() const { return thumbnailUrl; }
-    void setThumbnailUrl(const QString &value) { thumbnailUrl = value; }
-
-    const QString &getMediumThumbnailUrl() const { return mediumThumbnailUrl; }
-    void setMediumThumbnailUrl(const QString &value) { mediumThumbnailUrl = value; }
-
-    const QString &getLargeThumbnailUrl() const { return largeThumbnailUrl; }
-    void setLargeThumbnailUrl(const QString &value) { largeThumbnailUrl = value; }
-
     int getDuration() const { return duration; }
     void setDuration(int value);
     const QString &getFormattedDuration() const { return formattedDuration; }
@@ -81,7 +72,7 @@ public:
 
     void loadStreamUrl();
     const QString &getStreamUrl() { return streamUrl; }
-    bool isLoadingStreamUrl() const { return ytVideo != nullptr; }
+    bool isLoadingStreamUrl() const;
     void abortLoadStreamUrl();
 
     const QString &getId() const { return id; }
@@ -90,15 +81,16 @@ public:
     License getLicense() const { return license; }
     void setLicense(License value) { license = value; }
 
+    const auto &getThumbs() const { return thumbs; }
+    void addThumb(int width, int height, QString url);
+    VariantPromise &loadThumb(QSize size, qreal pixelRatio);
+
 signals:
-    void gotThumbnail();
-    void gotMediumThumbnail(const QByteArray &bytes);
-    void gotLargeThumbnail(const QByteArray &bytes);
     void gotStreamUrl(const QString &videoUrl, const QString &audioUrl);
     void errorStreamUrl(const QString &message);
+    void changed();
 
 private slots:
-    void setThumbnail(const QByteArray &bytes);
     void streamUrlLoaded(const QString &streamUrl, const QString &audioUrl);
 
 private:
@@ -111,10 +103,6 @@ private:
     QString channelId;
     QString webpage;
     QString streamUrl;
-    QPixmap thumbnail;
-    QString thumbnailUrl;
-    QString mediumThumbnailUrl;
-    QString largeThumbnailUrl;
     int duration;
     QString formattedDuration;
 
@@ -126,10 +114,11 @@ private:
     QString id;
     int definitionCode;
 
-    bool loadingThumbnail;
-
     YTVideo *ytVideo;
     YTJSVideo *ytjsVideo;
+
+    QVector<YTThumb> thumbs;
+    bool thumbsNeedSorting = false;
 };
 
 // This is required in order to use QPointer<Video> as a QVariant
index a2eacf7eb540c81c7743ebd89f2a0b5fc8d7340c..2e4adf7613ad8f28615eddb3c0241f2d49a29c61 100644 (file)
@@ -19,12 +19,13 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "videosourcewidget.h"
-#include "videosource.h"
-#include "video.h"
 #include "fontutils.h"
-#include "iconutils.h"
 #include "http.h"
 #include "httputils.h"
+#include "iconutils.h"
+#include "variantpromise.h"
+#include "video.h"
+#include "videosource.h"
 
 VideoSourceWidget::VideoSourceWidget(VideoSource *videoSource, QWidget *parent)
     : GridWidget(parent),
@@ -32,9 +33,7 @@ VideoSourceWidget::VideoSourceWidget(VideoSource *videoSource, QWidget *parent)
       lastPixelRatio(0) {
     videoSource->setParent(this);
     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-
     loadPreview();
-
     connect(this, SIGNAL(activated()), SLOT(activate()));
 }
 
@@ -50,13 +49,15 @@ void VideoSourceWidget::previewVideo(const QVector<Video *> &videos) {
         return;
     }
     Video *video = videos.at(0);
-    lastPixelRatio = window()->devicePixelRatio();
-    bool needLargeThumb = lastPixelRatio > 1.0 || window()->width() > 1000;
-    QString url =  needLargeThumb ? video->getLargeThumbnailUrl() : video->getMediumThumbnailUrl();
-    if (url.isEmpty()) url = video->getThumbnailUrl();
-    video->deleteLater();
-    QObject *reply = HttpUtils::yt().get(url);
-    connect(reply, SIGNAL(data(QByteArray)), SLOT(setPixmapData(QByteArray)));
+    lastPixelRatio = devicePixelRatio();
+
+    video->loadThumb(size(), lastPixelRatio)
+            .then([this](auto variant) { setPixmapData(variant.toByteArray()); })
+            .onFailed([](auto msg) { qDebug() << msg; })
+            .finally([videos] {
+                for (auto v : videos)
+                    v->deleteLater();
+            });
 }
 
 void VideoSourceWidget::setPixmapData(const QByteArray &bytes) {
@@ -75,7 +76,7 @@ QPixmap VideoSourceWidget::playPixmap() {
     const int s = height() / 2;
     const int padding = s / 8;
 
-    qreal ratio = window()->devicePixelRatio();
+    qreal ratio = devicePixelRatio();
     QPixmap playIcon = QPixmap(s * ratio, s * ratio);
     playIcon.setDevicePixelRatio(ratio);
     playIcon.fill(Qt::transparent);
@@ -101,8 +102,9 @@ QPixmap VideoSourceWidget::playPixmap() {
 
 void VideoSourceWidget::paintEvent(QPaintEvent *event) {
     GridWidget::paintEvent(event);
+    // if (devicePixelRatio() != lastPixelRatio) loadPreview();
+
     if (pixmap.isNull()) return;
-    if (window()->devicePixelRatio() != lastPixelRatio) loadPreview();
 
     QPainter p(this);
 
@@ -149,7 +151,7 @@ void VideoSourceWidget::paintEvent(QPaintEvent *event) {
     QString name = videoSource->getName();
     bool tooBig = false;
     p.save();
-    p.setFont(FontUtils::medium());
+    p.setFont(FontUtils::big());
     QRect textBox = p.boundingRect(nameBox, Qt::AlignCenter | Qt::TextWordWrap, name);
     if (textBox.height() > nameBox.height()) {
         p.setFont(font());
index 7c03e6fe30d58dcf10a8e4c2e6321a333185e945..35957763107cc8b4a83f4873cc9750081aa5a86b 100644 (file)
@@ -42,16 +42,9 @@ void IVListParser::parseItem(const QJsonObject &item) {
     video->setTitle(title);
     video->setDescription(item[QLatin1String("descriptionHtml")].toString());
 
-    const auto thumbnails = item[QLatin1String("videoThumbnails")].toArray();
-    for (const auto &thumbnail : thumbnails) {
-        auto q = thumbnail["quality"];
-        if (q == QLatin1String("medium")) {
-            video->setThumbnailUrl(thumbnail["url"].toString());
-        } else if (q == QLatin1String("high")) {
-            video->setMediumThumbnailUrl(thumbnail["url"].toString());
-        } else if (q == QLatin1String("sddefault")) {
-            video->setLargeThumbnailUrl(thumbnail["url"].toString());
-        }
+    const auto thumbs = item[QLatin1String("videoThumbnails")].toArray();
+    for (const auto &t : thumbs) {
+        video->addThumb(t["width"].toInt(), t["height"].toInt(), t["url"].toString());
     }
 
     video->setChannelTitle(item[QLatin1String("author")].toString());
index 5351349eadd5339c50e95a99f1dcac46ecd444c3..f7a708a04f9a99688ff3a868aeb462b9932d26ac 100644 (file)
@@ -6,10 +6,12 @@ include($$PWD/ytjs/ytjs.pri)
 
 HEADERS += \
     $$PWD/searchvideosource.h \
-    $$PWD/singlevideosource.h
+    $$PWD/singlevideosource.h \
+    $$PWD/ytthumb.h
 
 SOURCES += \
     $$PWD/searchvideosource.cpp \
-    $$PWD/singlevideosource.cpp
+    $$PWD/singlevideosource.cpp \
+    $$PWD/ytthumb.cpp
 
 
index 0baf1152d02acfd2f9e83a9fea722ab4e69f1332..e5dabe69d25a618daf962ebfcb41375c001cdea7 100644 (file)
@@ -6,6 +6,7 @@ HEADERS += \
     $$PWD/ytjschannelsource.h \
     $$PWD/ytjssearch.h \
     $$PWD/ytjssinglevideosource.h \
+    $$PWD/ytjstrending.h \
     $$PWD/ytjsvideo.h
 
 SOURCES += \
@@ -13,4 +14,5 @@ SOURCES += \
     $$PWD/ytjschannelsource.cpp \
     $$PWD/ytjssearch.cpp \
     $$PWD/ytjssinglevideosource.cpp \
+    $$PWD/ytjstrending.cpp \
     $$PWD/ytjsvideo.cpp
index e6ccdd30238f0e5421284d2c258222058181cbe8..de0e8e3bcba6474c2961455a03b716a486bdc471 100644 (file)
@@ -18,10 +18,10 @@ void YTJSChannel::load(const QString &channelId) {
                 const auto thumbs = obj["authorThumbnails"].toArray();
                 int maxFoundWidth = 0;
                 for (const auto &thumbObj : thumbs) {
-                    QString url = thumbObj["url"].toString();
                     int width = thumbObj["width"].toInt();
                     if (width > maxFoundWidth) {
                         maxFoundWidth = width;
+                        QString url = thumbObj["url"].toString();
                         thumbnailUrl = url;
                     }
                 }
index a38264006fb84e26633a76cf9b8b3fe5de056c49..d40e81fe39fad89c1f8452ed50ad970d36f1b1cf 100644 (file)
@@ -99,15 +99,9 @@ void YTJSChannelSource::loadVideos(int max, int startIndex) {
                     video->setDescription(desc);
 
                     const auto thumbs = i["videoThumbnails"].toArray();
-                    for (const auto &thumbObj : thumbs) {
-                        QString url = thumbObj["url"].toString();
-                        int width = thumbObj["width"].toInt();
-                        if (width >= 336)
-                            video->setLargeThumbnailUrl(url);
-                        else if (width >= 246)
-                            video->setMediumThumbnailUrl(url);
-                        else if (width >= 168)
-                            video->setThumbnailUrl(url);
+                    for (const auto &t : thumbs) {
+                        video->addThumb(t["width"].toInt(), t["height"].toInt(),
+                                        t["url"].toString());
                     }
 
                     int views = i["viewCount"].toInt();
@@ -134,13 +128,10 @@ void YTJSChannelSource::loadVideos(int max, int startIndex) {
                 emit gotVideos(videos);
                 emit finished(videos.size());
             })
-            .onError([this, &js, max, startIndex](auto &msg) {
+            .onError([this, max, startIndex](auto &msg) {
                 static int retries = 0;
                 if (retries < 3) {
                     qDebug() << "Retrying...";
-                    auto nam = js.getEngine().networkAccessManager();
-                    nam->clearAccessCache();
-                    nam->setCookieJar(new QNetworkCookieJar());
                     QTimer::singleShot(0, this,
                                        [this, max, startIndex] { loadVideos(max, startIndex); });
                     retries++;
index efdd0a0f51911b56b5633491dd50974376534dfb..65bd85dd00a0380ad41338f0632818dbbe8d2b62 100644 (file)
@@ -108,7 +108,7 @@ void YTJSSearch::loadVideos(int max, int startIndex) {
         jsMapSet.callWithInstance(filterMap, {name, value});
     };
 
-    addFilter("Type", "Video");
+    // addFilter("Type", "Video");
 
     switch (searchParams->sortBy()) {
     case SearchParams::SortByNewest:
@@ -127,6 +127,8 @@ void YTJSSearch::loadVideos(int max, int startIndex) {
         addFilter("Duration", "Short");
         break;
     case SearchParams::DurationMedium:
+        addFilter("Duration", "Medium");
+        break;
     case SearchParams::DurationLong:
         addFilter("Duration", "Long");
         break;
@@ -142,6 +144,9 @@ void YTJSSearch::loadVideos(int max, int startIndex) {
     case SearchParams::TimeMonth:
         addFilter("Upload date", "This month");
         break;
+    case SearchParams::TimeYear:
+        addFilter("Upload date", "This year");
+        break;
     }
 
     switch (searchParams->quality()) {
@@ -183,8 +188,11 @@ void YTJSSearch::loadVideos(int max, int startIndex) {
                     QString desc = i["description"].toString();
                     video->setDescription(desc);
 
-                    QString thumb = i["thumbnail"].toString();
-                    video->setThumbnailUrl(thumb);
+                    const auto thumbs = i["thumbnails"].toArray();
+                    for (const auto &t : thumbs) {
+                        video->addThumb(t["width"].toInt(), t["height"].toInt(),
+                                        t["url"].toString());
+                    }
 
                     int views = i["views"].toInt();
                     video->setViewCount(views);
@@ -211,13 +219,10 @@ void YTJSSearch::loadVideos(int max, int startIndex) {
                     emit finished(videos.size());
                 }
             })
-            .onError([this, &js, max, startIndex](auto &msg) {
+            .onError([this, max, startIndex](auto &msg) {
                 static int retries = 0;
                 if (retries < 3) {
                     qDebug() << "Retrying...";
-                    auto nam = js.getEngine().networkAccessManager();
-                    nam->clearAccessCache();
-                    nam->setCookieJar(new QNetworkCookieJar());
                     QTimer::singleShot(0, this,
                                        [this, max, startIndex] { loadVideos(max, startIndex); });
                     retries++;
index 2471ee9a4c47ad69c525532cda17b09c650f525b..058be3fe57f7979816d4509d719647cf695f3ebb 100644 (file)
@@ -3,6 +3,34 @@
 #include "js.h"
 #include "video.h"
 
+namespace {
+
+QDateTime parsePublishedText(const QString &s) {
+    int num = 0;
+    const auto parts = s.splitRef(' ');
+    for (const auto &part : parts) {
+        num = part.toInt();
+        if (num > 0) break;
+    }
+    if (num == 0) return QDateTime();
+
+    auto now = QDateTime::currentDateTimeUtc();
+    if (s.contains("hour")) {
+        return now.addSecs(-num * 3600);
+    } else if (s.contains("day")) {
+        return now.addDays(-num);
+    } else if (s.contains("week")) {
+        return now.addDays(-num * 7);
+    } else if (s.contains("month")) {
+        return now.addMonths(-num);
+    } else if (s.contains("year")) {
+        return now.addDays(-num * 365);
+    }
+    return QDateTime();
+}
+
+} // namespace
+
 YTJSSingleVideoSource::YTJSSingleVideoSource(QObject *parent)
     : VideoSource(parent), video(nullptr) {}
 
@@ -26,12 +54,12 @@ void YTJSSingleVideoSource::loadVideos(int max, int startIndex) {
                 if (aborted) return;
 
                 auto obj = doc.object();
-                // qDebug() << doc.toJson();
 
                 auto parseVideoObject = [](QJsonObject i) {
                     Video *video = new Video();
 
                     QString id = i["id"].toString();
+                    if (id.isEmpty()) id = i["videoId"].toString();
                     video->setId(id);
 
                     QString title = i["title"].toString();
@@ -39,31 +67,36 @@ void YTJSSingleVideoSource::loadVideos(int max, int startIndex) {
 
                     QString desc = i["description"].toString();
                     if (desc.isEmpty()) desc = i["desc"].toString();
+                    if (desc.isEmpty()) desc = i["shortDescription"].toString();
+
                     video->setDescription(desc);
 
                     const auto thumbs = i["thumbnails"].toArray();
-                    for (const auto &thumb : thumbs) {
-                        QString url = thumb["url"].toString();
-                        int width = thumb["width"].toInt();
-                        if (width >= 336)
-                            video->setLargeThumbnailUrl(url);
-                        else if (width >= 246)
-                            video->setMediumThumbnailUrl(url);
-                        else if (width >= 168)
-                            video->setThumbnailUrl(url);
+                    for (const auto &t : thumbs) {
+                        video->addThumb(t["width"].toInt(), t["height"].toInt(),
+                                        t["url"].toString());
                     }
 
-                    int views = i["view_count"].toInt();
+                    qDebug() << i["view_count"] << i["viewCount"];
+                    int views = i["view_count"].toString().toInt();
+                    if (views == 0) views = i["viewCount"].toString().toInt();
                     video->setViewCount(views);
 
                     int duration = i["length_seconds"].toInt();
-                    video->setViewCount(duration);
-
-                    QString channelId = i["ucid"].toString();
-                    video->setChannelId(channelId);
+                    video->setDuration(duration);
+
+                    auto author = i["author"];
+                    if (author.isObject()) {
+                        auto authorObject = author.toObject();
+                        video->setChannelId(authorObject["id"].toString());
+                        video->setChannelTitle(authorObject["name"].toString());
+                    } else if (author.isString()) {
+                        video->setChannelId(i["ucid"].toString());
+                        video->setChannelTitle(author.toString());
+                    }
 
-                    QString channelName = i["author"].toString();
-                    video->setChannelTitle(channelName);
+                    auto published = parsePublishedText(i["published"].toString());
+                    if (published.isValid()) video->setPublished(published);
 
                     return video;
                 };
@@ -71,8 +104,12 @@ void YTJSSingleVideoSource::loadVideos(int max, int startIndex) {
                 QVector<Video *> videos;
 
                 if (!video) {
-                    // parse video details
-                    videos << parseVideoObject(obj["videoDetails"].toObject());
+                    // first video
+                    auto video = parseVideoObject(obj["videoDetails"].toObject());
+                    videos << video;
+                    name = video->getTitle();
+                    qDebug() << "Emitting name changed" << name;
+                    emit nameChanged(name);
                 }
 
                 const auto items = obj["related_videos"].toArray();
diff --git a/src/yt/ytjs/ytjstrending.cpp b/src/yt/ytjs/ytjstrending.cpp
new file mode 100644 (file)
index 0000000..d62dfb5
--- /dev/null
@@ -0,0 +1,104 @@
+#include "ytjstrending.h"
+
+#include "js.h"
+#include "video.h"
+
+namespace {
+
+QDateTime parsePublishedText(const QString &s) {
+    int num = 0;
+    const auto parts = s.splitRef(' ');
+    for (const auto &part : parts) {
+        num = part.toInt();
+        if (num > 0) break;
+    }
+    if (num == 0) return QDateTime();
+
+    auto now = QDateTime::currentDateTimeUtc();
+    if (s.contains("hour")) {
+        return now.addSecs(-num * 3600);
+    } else if (s.contains("day")) {
+        return now.addDays(-num);
+    } else if (s.contains("week")) {
+        return now.addDays(-num * 7);
+    } else if (s.contains("month")) {
+        return now.addMonths(-num);
+    } else if (s.contains("year")) {
+        return now.addDays(-num * 365);
+    }
+    return QDateTime();
+}
+
+} // namespace
+
+YTJSTrending::YTJSTrending(QString name, QVariantMap params, QObject *parent)
+    : VideoSource(parent), name(name), params(params) {}
+
+void YTJSTrending::loadVideos(int max, int startIndex) {
+    aborted = false;
+
+    auto &js = JS::instance();
+
+    QJSValue options = js.getEngine().toScriptValue(params);
+
+    js.callFunction(new JSResult(this), "trending", {options})
+            .onJson([this](auto &doc) {
+                const auto items = doc.array();
+
+                QVector<Video *> videos;
+                videos.reserve(items.size());
+
+                for (const auto &i : items) {
+                    QString type = i["type"].toString();
+                    if (type != "video") continue;
+
+                    Video *video = new Video();
+
+                    QString id = i["videoId"].toString();
+                    video->setId(id);
+
+                    QString title = i["title"].toString();
+                    video->setTitle(title);
+
+                    QString desc = i["description"].toString();
+                    if (desc.isEmpty()) desc = i["desc"].toString();
+                    video->setDescription(desc);
+
+                    const auto thumbs = i["videoThumbnails"].toArray();
+                    for (const auto &t : thumbs) {
+                        video->addThumb(t["width"].toInt(), t["height"].toInt(),
+                                        t["url"].toString());
+                    }
+
+                    int views = i["viewCount"].toInt();
+                    video->setViewCount(views);
+
+                    int duration = i["lengthSeconds"].toInt();
+                    video->setDuration(duration);
+
+                    auto published = parsePublishedText(i["publishedText"].toString());
+                    if (published.isValid()) video->setPublished(published);
+
+                    QString channelName = i["author"].toString();
+                    video->setChannelTitle(channelName);
+                    QString channelId = i["authorId"].toString();
+                    video->setChannelId(channelId);
+
+                    videos << video;
+                }
+
+                emit gotVideos(videos);
+                emit finished(videos.size());
+            })
+            .onError([this, max, startIndex](auto &msg) {
+                static int retries = 0;
+                if (retries < 3) {
+                    qDebug() << "Retrying...";
+                    QTimer::singleShot(0, this,
+                                       [this, max, startIndex] { loadVideos(max, startIndex); });
+                    retries++;
+                } else {
+                    emit error(msg);
+                }
+            });
+}
diff --git a/src/yt/ytjs/ytjstrending.h b/src/yt/ytjs/ytjstrending.h
new file mode 100644 (file)
index 0000000..a43b507
--- /dev/null
@@ -0,0 +1,23 @@
+#ifndef YTJSTRENDING_H
+#define YTJSTRENDING_H
+
+#include "videosource.h"
+
+class Video;
+
+class YTJSTrending : public VideoSource {
+    Q_OBJECT
+
+public:
+    YTJSTrending(QString name, QVariantMap params, QObject *parent = 0);
+    void loadVideos(int max, int startIndex);
+    void abort() { aborted = true; }
+    QString getName() { return name; }
+
+private:
+    const QString name;
+    const QVariantMap params;
+    bool aborted = false;
+};
+
+#endif // YTJSTRENDING_H
diff --git a/src/yt/ytthumb.cpp b/src/yt/ytthumb.cpp
new file mode 100644 (file)
index 0000000..d5659eb
--- /dev/null
@@ -0,0 +1,28 @@
+#include "ytthumb.h"
+
+#include "http.h"
+#include "httpreply.h"
+#include "httputils.h"
+
+YTThumb::YTThumb(int width, int height, const QString &url)
+    : width(width), height(height), url(url) {}
+
+VariantPromise &YTThumb::load(QObject *parent) {
+    qDebug() << parent;
+    if (promise) {
+        qDebug() << "Already loading" << promise;
+        return *promise;
+    }
+    promise = new VariantPromise(parent);
+    promise->connect(HttpUtils::yt().get(url), &HttpReply::finished, promise, [this](auto &reply) {
+        // clear promise member before emitting signals
+        auto promise2 = promise;
+        promise = nullptr;
+        if (reply.isSuccessful()) {
+            promise2->resolve(reply.body());
+        } else {
+            promise2->reject(reply.reasonPhrase());
+        }
+    });
+    return *promise;
+}
diff --git a/src/yt/ytthumb.h b/src/yt/ytthumb.h
new file mode 100644 (file)
index 0000000..fb6112c
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef YTTHUMB_H
+#define YTTHUMB_H
+
+#include <QtCore>
+
+#include "variantpromise.h"
+
+class YTThumb {
+public:
+    YTThumb() {} // needed by QVector
+    YTThumb(int width, int height, const QString &url);
+    int getWidth() const { return width; }
+    int getHeight() const { return height; }
+    const QString &getUrl() const { return url; }
+
+    VariantPromise &load(QObject *parent);
+
+private:
+    int width;
+    int height;
+    QString url;
+    VariantPromise *promise = nullptr;
+};
+
+#endif // YTTHUMB_H
index 53e9e244f1e056c6c880cfa37e31ab462aa53636..604be190c25179ce780525aa3cc2cc4553ca3371 100644 (file)
@@ -59,10 +59,7 @@ void YT3ListParser::parseItem(const QJsonObject &item) {
     video->setDescription(snippet[QLatin1String("description")].toString());
 
     QJsonObject thumbnails = snippet[QLatin1String("thumbnails")].toObject();
-    QLatin1String url("url");
-    video->setThumbnailUrl(thumbnails[QLatin1String("medium")].toObject()[url].toString());
-    video->setMediumThumbnailUrl(thumbnails[QLatin1String("high")].toObject()[url].toString());
-    video->setLargeThumbnailUrl(thumbnails[QLatin1String("standard")].toObject()[url].toString());
+    // TODO
 
     video->setChannelTitle(snippet[QLatin1String("channelTitle")].toString());
 
index f968a72981dd3349cddf6b56f9cbce46a0357c9c..97f9f0e92bed6a5ffe2d30398a4512ab133e9ca3 100644 (file)
@@ -95,8 +95,8 @@ const YTRegion &YTRegions::localRegion() {
     return region;
 }
 
-const YTRegion &YTRegions::worldwideRegion() {
-    static const YTRegion region = {"", tr("Worldwide")};
+const YTRegion &YTRegions::defaultRegion() {
+    static const YTRegion region = {"US", tr("United States")};
     return region;
 }
 
@@ -115,11 +115,11 @@ const YTRegion &YTRegions::currentRegion() {
 }
 
 const YTRegion &YTRegions::regionById(const QString &id) {
-    if (id.isEmpty()) return worldwideRegion();
+    if (id.isEmpty()) return defaultRegion();
     for (const YTRegion &r : list()) {
         if (r.id == id) return r;
     }
-    return worldwideRegion();
+    return defaultRegion();
 }
 
 QIcon YTRegions::iconForRegionId(const QString &regionId) {
index 6dfe4b09fd8e955fa6bcd60c155f241e588fb17b..4e10ef3b97b8d9a0aa729288794fb7de54a706ea 100644 (file)
@@ -38,7 +38,7 @@ class YTRegions : public QObject {
 public:
     static const QVector<YTRegion> & list();
     static const YTRegion & localRegion();
-    static const YTRegion & worldwideRegion();
+    static const YTRegion & defaultRegion();
     static void setRegion(const QString &regionId);
     static QString currentRegionId();
     static const YTRegion &currentRegion();