From: Jakob Haufe Date: Wed, 15 Sep 2021 07:25:21 +0000 (+0000) Subject: New upstream version 3.9.1 X-Git-Url: https://git.sur5r.net/?a=commitdiff_plain;h=2e524d4ed49280113efb6318b32d7f8931c5ccbd;p=minitube New upstream version 3.9.1 --- diff --git a/lib/http/src/http.cpp b/lib/http/src/http.cpp index 5df34a6..9d85e4f 100644 --- a/lib/http/src/http.cpp +++ b/lib/http/src/http.cpp @@ -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; } diff --git a/lib/http/src/networkhttpreply.cpp b/lib/http/src/networkhttpreply.cpp index c27d77d..08b1735 100644 --- a/lib/http/src/networkhttpreply.cpp +++ b/lib/http/src/networkhttpreply.cpp @@ -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(); diff --git a/lib/js/js.cpp b/lib/js/js.cpp index 2390ef5..8840064 100644 --- a/lib/js/js.cpp +++ b/lib/js/js.cpp @@ -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"; diff --git a/lib/js/js.h b/lib/js/js.h index 8f0e33a..104bc46 100644 --- a/lib/js/js.h +++ b/lib/js/js.h @@ -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(); diff --git a/lib/js/jsnamfactory.cpp b/lib/js/jsnamfactory.cpp index 3490916..dd14c40 100644 --- a/lib/js/jsnamfactory.cpp +++ b/lib/js/jsnamfactory.cpp @@ -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 index 0000000..e3fd3da --- /dev/null +++ b/lib/promises/LICENSE @@ -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 index 0000000..4b385f0 --- /dev/null +++ b/lib/promises/emptypromise.h @@ -0,0 +1,33 @@ +#ifndef EMPTYPROMISE_H +#define EMPTYPROMISE_H + +#include + +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 EmptyPromise &then(Functor func) { + connect(this, &EmptyPromise::resolve, this, func); + return *this; + } + template EmptyPromise &onFailed(Functor func) { + connect(this, &EmptyPromise::reject, this, func); + return *this; + } + template 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 index 0000000..5bf1c87 --- /dev/null +++ b/lib/promises/promise.h @@ -0,0 +1,46 @@ +#ifndef PROMISE_H +#define PROMISE_H + +#include + +/// 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 BasePromise &then(Function func) { + connect(this, &BasePromise::resolve, this, func); + return *this; + } + template BasePromise &onFailed(Function func) { + connect(this, &BasePromise::reject, this, func); + return *this; + } + template BasePromise &finally(Function func) { + connect(this, &BasePromise::destroyed, this, func); + return *this; + } + +signals: + void resolve(); + void reject(const QString &message); +}; + +template 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 index 0000000..aee3f98 --- /dev/null +++ b/lib/promises/promises.pri @@ -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 index 0000000..6cc3555 --- /dev/null +++ b/lib/promises/variantpromise.h @@ -0,0 +1,33 @@ +#ifndef VARIANTPROMISE_H +#define VARIANTPROMISE_H + +#include + +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 VariantPromise &then(Function func) { + connect(this, &VariantPromise::resolve, this, func); + return *this; + } + template VariantPromise &onFailed(Function func) { + connect(this, &VariantPromise::reject, this, func); + return *this; + } + template 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 diff --git a/lib/updater/src/updater.cpp b/lib/updater/src/updater.cpp index 3efa41c..564f2d2 100644 --- a/lib/updater/src/updater.cpp +++ b/lib/updater/src/updater.cpp @@ -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; diff --git a/locale/fi.ts b/locale/fi.ts index 7c204af..af54212 100644 --- a/locale/fi.ts +++ b/locale/fi.ts @@ -223,7 +223,7 @@ %n year(s) ago - + %n vuotta sitten%n vuotta sitten @@ -1038,19 +1038,19 @@ This year - + Tänä vuonna HD - + HD 4K - + 4K HDR - + HDR @@ -1111,7 +1111,7 @@ An update is ready to be installed. Quit and install update. - + Päivitys on valmiina asennettavaksi. Poistu sovelluksesta asentaaksesi päivityksen. @@ -1167,23 +1167,23 @@ Trending - + Trendaavat Music - + Musiikki News - + Uutiset Movies - + Elokuvat Gaming - + Pelaaminen @@ -1201,31 +1201,31 @@ Updater Check for Updates... - + Tarkista onko päivityksiä saatavilla... Version %1 is available... - + Versio %1 on saatavilla... Downloading version %1... - + Ladataan versiota %1... Restart to Update - + Uudelleenkäynnistä päivittääksesi Version %1 download failed - + Version %1 lataaminen epäonnistui Check for Updates - + Tarkista onko päivityksiä saatavilla Download Update - + Lataa päivitys Downloading update... @@ -1233,19 +1233,19 @@ Retry Update Download - + Yritä ladata päivitystä uudelleen You have the latest version. - + Käytössä on jo viimeisin versio. Version %1 is available. - + Versio %1 on saatavissa. An update has been downloaded and is ready to be installed. - + Päivitys ladattiin ja on nyt valmis asennettavaksi. @@ -1461,18 +1461,18 @@ updater::DefaultUpdater There are currently no updates available. - + Päivityksiä ei ole tällä hetkellä saatavilla. updater::Dialog You already have the latest version - + Käytössä on jo viimeisin versio Downloading %1 %2... - + Ladataan %1 %2... A new version of %1 is available! @@ -1492,7 +1492,7 @@ Download Update - + Lataa päivitys \ No newline at end of file diff --git a/locale/ja_JP.ts b/locale/ja_JP.ts index c75e281..7ada89c 100644 --- a/locale/ja_JP.ts +++ b/locale/ja_JP.ts @@ -223,7 +223,7 @@ %n year(s) ago - + %n年前 diff --git a/locale/ro.ts b/locale/ro.ts index 389af36..2f9cc3d 100644 --- a/locale/ro.ts +++ b/locale/ro.ts @@ -27,11 +27,11 @@ Powered by %1 - + Alimentat de %1 Open-source software - + Software Sursă-Deschisă Icon designed by %1. @@ -107,7 +107,7 @@ You have %n new video(s) - + Aveți un videoclip nouAveți %n videoclipuri noiAveți %n de videoclipuri noi diff --git a/minitube.pro b/minitube.pro index e48dbb2..71420f3 100644 --- a/minitube.pro +++ b/minitube.pro @@ -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) diff --git a/org.tordini.flavio.minitube.metainfo.xml b/org.tordini.flavio.minitube.metainfo.xml index 9815df0..884b05c 100644 --- a/org.tordini.flavio.minitube.metainfo.xml +++ b/org.tordini.flavio.minitube.metainfo.xml @@ -2,6 +2,7 @@ Minitube org.tordini.flavio.minitube + minitube.desktop CC0-1.0 GPL-3.0-or-later YouTube app @@ -36,6 +37,10 @@ https://flavio.tordini.org/donate https://www.transifex.com/flaviotordini/minitube/ + + + + diff --git a/resources.qrc b/resources.qrc index 9933d32..c229b26 100644 --- a/resources.qrc +++ b/resources.qrc @@ -52,5 +52,6 @@ images/app@2x.png images/64x64/app.png images/64x64/app@2x.png + flags/us.png diff --git a/src/aggregatevideosource.cpp b/src/aggregatevideosource.cpp index 95300f8..cf9b6a8 100644 --- a/src/aggregatevideosource.cpp +++ b/src/aggregatevideosource.cpp @@ -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; diff --git a/src/channelaggregator.cpp b/src/channelaggregator.cpp index d137792..8097af1 100644 --- a/src/channelaggregator.cpp +++ b/src/channelaggregator.cpp @@ -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(); diff --git a/src/jsfunctions.cpp b/src/jsfunctions.cpp index 679d1e3..96f221f 100644 --- a/src/jsfunctions.cpp +++ b/src/jsfunctions.cpp @@ -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() { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8b001ac..610c0d1 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -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(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); diff --git a/src/mediaview.cpp b/src/mediaview.cpp index e0aebfd..5d74d0b 100644 --- a/src/mediaview.cpp +++ b/src/mediaview.cpp @@ -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(); } } diff --git a/src/playlistitemdelegate.cpp b/src/playlistitemdelegate.cpp index 378bba3..33d431e 100644 --- a/src/playlistitemdelegate.cpp +++ b/src/playlistitemdelegate.cpp @@ -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().data(); + Video *video = index.data(VideoRole).value().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(); 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(); diff --git a/src/playlistmodel.cpp b/src/playlistmodel.cpp index be08194..f2e0e5f 100644 --- a/src/playlistmodel.cpp +++ b/src/playlistmodel.cpp @@ -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