From b909b4d1892bf5f77e6834ba96ff122b1c4aeb99 Mon Sep 17 00:00:00 2001 From: NamedKitten Date: Sat, 8 Dec 2018 22:21:12 +0000 Subject: [PATCH] [UI+Backend] Improved playlist menu and added thumbnails. --- CMakeLists.txt | 2 + src/MpvPlayerBackend.cpp | 2 +- src/Process.cpp | 22 +++++ src/Process.h | 14 +++ src/ThumbnailCache.cpp | 59 ++++++++++++ src/ThumbnailCache.h | 32 +++++++ src/main.cpp | 43 ++------- src/qml/Dialogs/PlaylistDialog.qml | 149 +++++++++++++++++++++++++++-- src/qml/Items/ThumbnailProcess.qml | 20 ++++ src/qml/Items/TitleProcess.qml | 15 +++ src/qml/qml.qrc | 2 + src/utils.cpp | 52 +++++++++- src/utils.hpp | 4 +- tmp | 77 +++++++++++++++ 14 files changed, 442 insertions(+), 51 deletions(-) create mode 100644 src/Process.cpp create mode 100644 src/Process.h create mode 100644 src/ThumbnailCache.cpp create mode 100644 src/ThumbnailCache.h create mode 100644 src/qml/Items/ThumbnailProcess.qml create mode 100644 src/qml/Items/TitleProcess.qml create mode 100644 tmp diff --git a/CMakeLists.txt b/CMakeLists.txt index e6c09c0..bd3df40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ set(SOURCES src/DirectMpvPlayerBackend.cpp src/utils.cpp src/enums.hpp + src/Process.cpp + src/ThumbnailCache.cpp ) if(MPV_VERSION VERSION_GREATER_EQUAL "1.28.0") diff --git a/src/MpvPlayerBackend.cpp b/src/MpvPlayerBackend.cpp index 93acb24..c8480e3 100644 --- a/src/MpvPlayerBackend.cpp +++ b/src/MpvPlayerBackend.cpp @@ -132,7 +132,7 @@ MpvPlayerBackend::MpvPlayerBackend(QQuickItem* parent) if (!mpv) throw std::runtime_error("could not create mpv context"); - mpv_set_option_string(mpv, "terminal", "yes"); + mpv_set_option_string(mpv, "terminal", "off"); mpv_set_option_string(mpv, "msg-level", "all=v"); // Fix? diff --git a/src/Process.cpp b/src/Process.cpp new file mode 100644 index 0000000..b7e6e8b --- /dev/null +++ b/src/Process.cpp @@ -0,0 +1,22 @@ +#include "Process.h" + +Process::Process(QObject* parent) + : QProcess(parent) +{} + +void +Process::start(const QString& program, const QVariantList& arguments) +{ + QStringList args; + + for (int i = 0; i < arguments.length(); i++) + args << arguments[i].toString(); + + QProcess::start(program, args); +} + +QString +Process::getOutput() +{ + return QProcess::readAllStandardOutput(); +} diff --git a/src/Process.h b/src/Process.h new file mode 100644 index 0000000..d90a9f8 --- /dev/null +++ b/src/Process.h @@ -0,0 +1,14 @@ +#include +#include + +class Process : public QProcess +{ + Q_OBJECT + +public: + explicit Process(QObject* parent = 0); + + Q_INVOKABLE void start(const QString& program, const QVariantList& arguments); + + Q_INVOKABLE QString getOutput(); +}; diff --git a/src/ThumbnailCache.cpp b/src/ThumbnailCache.cpp new file mode 100644 index 0000000..dc7378b --- /dev/null +++ b/src/ThumbnailCache.cpp @@ -0,0 +1,59 @@ +#include "ThumbnailCache.h" +#include +#include + +ThumbnailCache::ThumbnailCache(QObject* parent) + : QObject(parent) + , manager(new QNetworkAccessManager(this)) +{ + cacheFolder = + QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/thumbs"); + if (!cacheFolder.exists()) { + cacheFolder.mkpath("."); + } +} + +void +ThumbnailCache::addURL(const QString& name, const QString& mediaURL) +{ + + QString hashedURL = QString( + QCryptographicHash::hash(name.toUtf8(), QCryptographicHash::Md5).toHex()); + QString cacheFilename = hashedURL + ".jpg"; + QString cachedFilePath = cacheFolder.absoluteFilePath(cacheFilename); + if (cacheFolder.exists(cacheFilename)) { + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + return; + } + + QString url(mediaURL); + QFileInfo isFile = QFileInfo(url); + if (isFile.exists()) { + QImageReader reader(url); + QImage image = reader.read(); + + image.save(cachedFilePath, "JPG"); + + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + return; + } + + QNetworkRequest request(url); + + QNetworkReply* reply = manager->get(request); + + connect(reply, &QNetworkReply::finished, [=] { + QByteArray response_data = reply->readAll(); + + QBuffer buffer(&response_data); + buffer.open(QIODevice::ReadOnly); + + QImageReader reader(&buffer); + QImage image = reader.read(); + + image.save(cachedFilePath, "JPG"); + + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + }); +} diff --git a/src/ThumbnailCache.h b/src/ThumbnailCache.h new file mode 100644 index 0000000..14bec86 --- /dev/null +++ b/src/ThumbnailCache.h @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ThumbnailCache : public QObject +{ + Q_OBJECT + +public: + explicit ThumbnailCache(QObject* parent = nullptr); + +public slots: + Q_INVOKABLE void addURL(const QString& name, const QString& url); + +signals: + void thumbnailReady(const QString& name, + const QString& url, + const QString& filePath); + +private: + QNetworkAccessManager* manager; + QDir cacheFolder; +}; diff --git a/src/main.cpp b/src/main.cpp index ead2582..6794853 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,23 +7,19 @@ #include "utils.hpp" #include +#include "Process.h" #include "enums.hpp" #include #include #include #include +#include #include - #ifdef WIN32 #include "setenv_mingw.hpp" #endif -#ifdef GIT_COMMIT_HASH -#include -#include -#include - -#endif +#include "ThumbnailCache.h" #ifdef __linux__ #include @@ -80,36 +76,7 @@ main(int argc, char* argv[]) } if (checkForUpdates) { - QString current_version = QString(GIT_COMMIT_HASH); - qDebug() << "Current Version: " << current_version; - - QNetworkRequest request(QUrl("https://api.github.com/repos/NamedKitten/" - "KittehPlayer/releases/tags/continuous")); - - QNetworkAccessManager nam; - QNetworkReply* reply = nam.get(request); - - while (!reply->isFinished()) { - qApp->processEvents(); - } - QByteArray response_data = reply->readAll(); - QJsonDocument json = QJsonDocument::fromJson(response_data); - - if (json["target_commitish"].toString().length() != 0) { - if (json["target_commitish"].toString().endsWith(current_version) == 0) { - qDebug() << "Latest Version: " << json["target_commitish"].toString(); - qDebug() << "Update Available. Please update ASAP."; - QProcess notifier; - notifier.setProcessChannelMode(QProcess::ForwardedChannels); - notifier.start("notify-send", - QStringList() << "KittehPlayer" - << "New update avalable!" - << "--icon=KittehPlayer"); - notifier.waitForFinished(); - } - } else { - qDebug() << "Couldn't check for new version."; - } + Utils::checkForUpdates(); } #endif @@ -148,6 +115,8 @@ main(int argc, char* argv[]) qRegisterMetaType("Enums.VolumeStatus"); qRegisterMetaType("Enums.Backends"); qRegisterMetaType("Enums.Commands"); + qmlRegisterType("player", 1, 0, "Process"); + qmlRegisterType("player", 1, 0, "ThumbnailCache"); qmlRegisterType("player", 1, 0, "Utils"); diff --git a/src/qml/Dialogs/PlaylistDialog.qml b/src/qml/Dialogs/PlaylistDialog.qml index f193e5e..ab8ed27 100644 --- a/src/qml/Dialogs/PlaylistDialog.qml +++ b/src/qml/Dialogs/PlaylistDialog.qml @@ -12,11 +12,99 @@ Dialog { height: Math.max(480, childrenRect.height * playlistListView.count) width: 720 modality: Qt.NonModal + property int thumbnailJobsRunning: 0 + property variant thumbnailJobs: [] + property int titleJobsRunning: 0 + property variant titleJobs: [] + + function addThumbnailToCache(name, output) { + output = output.replace("maxresdefault", "sddefault").split('\n')[0] + thumbnailCache.addURL(name, output) + thumbnailJobs.shift() + thumbnailJobsRunning -= 1 + } + + ThumbnailCache { + id: thumbnailCache + } + + Rectangle { + visible: false + id: titleGetter + signal titleFound(string name, string title) + } + + Timer { + interval: 100 + repeat: true + triggeredOnStart: true + running: true + onTriggered: { + if (thumbnailJobsRunning < 2) { + if (thumbnailJobs.length > 0) { + if (thumbnailJobs[0].startsWith( + "https://www.youtube.com/playlist?list=")) { + thumbnailJobs.shift() + return + } + var component = Qt.createComponent("ThumbnailProcess.qml") + var thumbnailerProcess = component.createObject( + playlistDialog, { + name: thumbnailJobs[0] + }) + if (String(titleJobs[0]).indexOf("://") !== -1) { + + thumbnailerProcess.start( + "youtube-dl", + ["--get-thumbnail", thumbnailJobs[0]]) + } else { + thumbnailerProcess.start( + "ffmpegthumbnailer", + ["-i", thumbnailJobs[0], "-o", "/tmp/" + Qt.md5( + thumbnailJobs[0]) + ".png"]) + } + + thumbnailJobsRunning += 1 + } + } + } + } + + Timer { + interval: 100 + repeat: true + triggeredOnStart: true + running: true + onTriggered: { + if (titleJobsRunning < 5) { + if (titleJobs.length > 0) { + if (titleJobs[0].startsWith( + "https://www.youtube.com/playlist?list=")) { + titleJobs.shift() + return + } + var component = Qt.createComponent("TitleProcess.qml") + var titleProcess = component.createObject(playlistDialog, { + name: titleJobs[0] + }) + titleProcess.start("youtube-dl", + ["--get-title", titleJobs[0]]) + titleJobs.shift() + titleJobsRunning += 1 + } + } + } + } + Connections { target: player enabled: true onPlaylistChanged: function (playlist) { playlistModel.clear() + thumbnailJobs = [] + titleJobs = [] + titleJobsRunning = 0 + thumbnailJobsRunning = 0 for (var thing in playlist) { var item = playlist[thing] playlistModel.append({ @@ -33,20 +121,59 @@ Dialog { id: playlistDelegate Item { id: playlistItem + property string itemURL: "" + property string itemTitle: "" width: playlistDialog.width height: childrenRect.height + function getText(title, filename) { + var itemText = "" + if (title.length > 0) { + itemText += 'Title: ' + title + "
" + } + if (filename.length > 0) { + itemText += 'Filename: ' + filename + } + return itemText + } + Connections { + target: thumbnailCache + onThumbnailReady: function (name, url, path) { + if (name == playlistItem.itemURL) { + thumbnail.source = path + } + } + } + + Connections { + target: titleGetter + onTitleFound: function (name, title) { + if (name == playlistItem.itemURL) { + titleJobsRunning -= 1 + playlistItem.itemTitle = title + } + } + } + + Image { + id: thumbnail + source: "" + height: source.toString().length > 1 ? 144 : 0 + width: source.toString().length > 1 ? 256 : 0 + } + Button { - width: parent.width + width: parent.width - 20 id: playlistItemButton font.pixelSize: 12 padding: 0 + anchors.left: thumbnail.right bottomPadding: 0 contentItem: Text { id: playlistItemText font: parent.font bottomPadding: 0 color: "white" - text: playlistItemButton.text + text: playlistItem.getText(itemTitle, itemURL) height: parent.height horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter @@ -64,14 +191,18 @@ Dialog { } Component.onCompleted: { - var itemText = "" + if (typeof playlistItemTitle !== "undefined") { - itemText += 'Title: ' + playlistItemTitle + "
" + playlistItem.itemTitle = playlistItemTitle + } else { + playlistDialog.titleJobs.push(playlistItemFilename) } if (typeof playlistItemFilename !== "undefined") { - itemText += 'Filename: ' + playlistItemFilename + playlistItem.itemURL = playlistItemFilename + } else { + playlistItem.itemURL = "" } - playlistItemText.text = itemText + playlistDialog.thumbnailJobs.push(playlistItemFilename) } } } @@ -85,6 +216,12 @@ Dialog { delegate: playlistDelegate highlight: Item { } + snapMode: ListView.SnapToItem + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { + active: playlistListView.count > 1 ? true : true + } focus: true } } diff --git a/src/qml/Items/ThumbnailProcess.qml b/src/qml/Items/ThumbnailProcess.qml new file mode 100644 index 0000000..6d18f44 --- /dev/null +++ b/src/qml/Items/ThumbnailProcess.qml @@ -0,0 +1,20 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.3 +import QtQuick.Window 2.11 +import Qt.labs.settings 1.0 +import Qt.labs.platform 1.0 as LabsPlatform +import player 1.0 + +Process { + id: thumbnailerProcess + property string name: "" + onFinished: function () { + if (String(name).indexOf("://") !== -1) { + playlistDialog.addThumbnailToCache(name, getOutput()) + } else { + playlistDialog.addThumbnailToCache(name, + "/tmp/" + Qt.md5(name) + ".png") + } + } +} diff --git a/src/qml/Items/TitleProcess.qml b/src/qml/Items/TitleProcess.qml new file mode 100644 index 0000000..80e7a4b --- /dev/null +++ b/src/qml/Items/TitleProcess.qml @@ -0,0 +1,15 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.3 +import QtQuick.Window 2.11 +import Qt.labs.settings 1.0 +import Qt.labs.platform 1.0 as LabsPlatform +import player 1.0 + +Process { + id: titleProcess + property string name: "" + onReadyRead: function () { + titleGetter.titleFound(name, getOutput()) + } +} diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc index 1d7e451..0dda688 100644 --- a/src/qml/qml.qrc +++ b/src/qml/qml.qrc @@ -27,6 +27,8 @@ Items/ChapterMarkerItem.qml Items/TrackItem.qml Items/AudioDeviceItem.qml + Items/ThumbnailProcess.qml + Items/TitleProcess.qml Items/CustomMenuItem.qml Dialogs/PlaylistDialog.qml icons/YouTube/play.svg diff --git a/src/utils.cpp b/src/utils.cpp index 3472279..5e92ad4 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -3,11 +3,14 @@ #include #include +#include #include #include +#include #include #include #include +#include #ifdef __linux__ #ifdef ENABLE_X11 @@ -34,6 +37,45 @@ launchAboutQt() qapp->aboutQt(); } +void +checkForUpdates() +{ +#ifdef GIT_COMMIT_HASH + QString current_version = QString(GIT_COMMIT_HASH); +#else + QString current_version = QString("Unknown"); +#endif + qDebug() << "Current Version: " << current_version; + + QNetworkRequest request(QUrl("https://api.github.com/repos/NamedKitten/" + "KittehPlayer/releases/tags/continuous")); + + QNetworkAccessManager nam; + QNetworkReply* reply = nam.get(request); + + while (!reply->isFinished()) { + qApp->processEvents(); + } + QByteArray response_data = reply->readAll(); + QJsonDocument json = QJsonDocument::fromJson(response_data); + + if (json["target_commitish"].toString().length() != 0) { + if (json["target_commitish"].toString().endsWith(current_version) == 0) { + qDebug() << "Latest Version: " << json["target_commitish"].toString(); + qDebug() << "Update Available. Please update ASAP."; + QProcess notifier; + notifier.setProcessChannelMode(QProcess::ForwardedChannels); + notifier.start("notify-send", + QStringList() << "KittehPlayer" + << "New update avalable!" + << "--icon=KittehPlayer"); + notifier.waitForFinished(); + } + } else { + qDebug() << "Couldn't check for new version."; + } +} + void updateAppImage() { @@ -50,19 +92,19 @@ updateAppImage() } // https://www.youtube.com/watch?v=nXaxk27zwlk&feature=youtu.be&t=56m34s -int +inline const int fast_mod(const int input, const int ceil) { return input >= ceil ? input % ceil : input; } QString -createTimestamp(int seconds) +createTimestamp(const int seconds) { - int s = fast_mod(seconds, 60); - int m = fast_mod(seconds, 3600) / 60; - int h = fast_mod(seconds, 86400) / 3600; + const int s = fast_mod(seconds, 60); + const int m = fast_mod(seconds, 3600) / 60; + const int h = fast_mod(seconds, 86400) / 3600; if (h > 0) { return QString::asprintf("%02d:%02d:%02d", h, m, s); diff --git a/src/utils.hpp b/src/utils.hpp index f36d4e9..11fa900 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -14,9 +14,9 @@ SetScreensaver(WId wid, bool on); void AlwaysOnTop(WId wid, bool on); void +checkForUpdates(); +void updateAppImage(); -int -fast_mod(const int input, const int ceil); QString createTimestamp(int seconds); void diff --git a/tmp b/tmp new file mode 100644 index 0000000..257bd3f --- /dev/null +++ b/tmp @@ -0,0 +1,77 @@ +#include "ThumbnailCache.h" +#include +#include + +ThumbnailCache::ThumbnailCache(QObject* parent) + : QObject(parent) + , manager(new QNetworkAccessManager(this)) +{ + cacheFolder = + QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/thumbs"); + if (!cacheFolder.exists()) { + cacheFolder.mkpath("."); + } +} + +void +ThumbnailCache::addURL(const QString& mediaURL) +{ + + QString hashedURL = + QString(QCryptographicHash::hash(mediaURL.toUtf8(), QCryptographicHash::Md5) + .toHex()); + qDebug() << hashedURL; + QString cacheFilename = hashedURL + ".png"; + QString cachedFilePath = cacheFolder.absoluteFilePath(cacheFilename); + if (cacheFolder.exists(cacheFilename)) { + qDebug() << mediaURL << " is in cache at " << cachedFilePath; + emit thumbnailReady(mediaURL, "file://" + cachedFilePath); + return; + } + QProcess* thumbnailerProcess = new QProcess(this); + QStringList params; + params << "--get-thumbnail" << mediaURL; + + connect(thumbnailerProcess, QOverload::of(&QProcess::finished), + [=](int exitCode, QProcess::ExitStatus exitStatus){ /* ... */ }); + + connect(thumbnailerProcess, static_cast(&QProcess::finished), + +[=] (int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "finished. Exit code: " + exitCode ; + }); + + + /* + connect(thumbnailerProcess, + static_cast( + &QProcess::finished), + [=](int, QProcess::ExitStatus) { + qDebug() << "NYA!"; + QString url(thumbnailerProcess->readAll()); + + QNetworkRequest request(url); + + QNetworkReply* reply = manager->get(request); + + connect(reply, &QNetworkReply::finished, [=] { + QByteArray response_data = reply->readAll(); + + QPixmap p; + p.loadFromData(response_data); + p.save(cachedFilePath, "PNG"); + QString extension = + url.right(url.length() - url.lastIndexOf(".") - 1).toUpper(); + qDebug() << extension; + + emit thumbnailReady(mediaURL, "file://" + cachedFilePath); + }); + });*/ + thumbnailerProcess->start("youtube-dl", params); + while (thumbnailerProcess->exitStatus() == QProcess::NormalExit) { + qDebug() << "NYA!"; + +} +}