1
0
Fork 0

[UI+Backend] Improved playlist menu and added thumbnails.

This commit is contained in:
NamedKitten 2018-12-08 22:21:12 +00:00
parent 7b8beeb57b
commit b909b4d189
14 changed files with 442 additions and 51 deletions

View file

@ -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")

View file

@ -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?

22
src/Process.cpp Normal file
View file

@ -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();
}

14
src/Process.h Normal file
View file

@ -0,0 +1,14 @@
#include <QProcess>
#include <QVariant>
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();
};

59
src/ThumbnailCache.cpp Normal file
View file

@ -0,0 +1,59 @@
#include "ThumbnailCache.h"
#include <QCryptographicHash>
#include <QImageReader>
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);
});
}

32
src/ThumbnailCache.h Normal file
View file

@ -0,0 +1,32 @@
#include <QApplication>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QObject>
#include <QProcessEnvironment>
#include <QQmlApplicationEngine>
#include <QSequentialIterable>
#include <QString>
#include <QVariant>
#include <QtCore>
#include <QtNetwork>
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;
};

View file

@ -7,23 +7,19 @@
#include "utils.hpp"
#include <cstdlib>
#include "Process.h"
#include "enums.hpp"
#include <QApplication>
#include <QProcessEnvironment>
#include <QQmlApplicationEngine>
#include <QtCore>
#include <QtQml>
#include <stdbool.h>
#ifdef WIN32
#include "setenv_mingw.hpp"
#endif
#ifdef GIT_COMMIT_HASH
#include <QJsonDocument>
#include <QSequentialIterable>
#include <QtNetwork>
#endif
#include "ThumbnailCache.h"
#ifdef __linux__
#include <initializer_list>
@ -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>("Enums.VolumeStatus");
qRegisterMetaType<Enums::Backends>("Enums.Backends");
qRegisterMetaType<Enums::Commands>("Enums.Commands");
qmlRegisterType<Process>("player", 1, 0, "Process");
qmlRegisterType<ThumbnailCache>("player", 1, 0, "ThumbnailCache");
qmlRegisterType<UtilsClass>("player", 1, 0, "Utils");

View file

@ -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 += '<b>Title:</b> ' + title + "<br>"
}
if (filename.length > 0) {
itemText += '<b>Filename:</b> ' + 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 += '<b>Title:</b> ' + playlistItemTitle + "<br>"
playlistItem.itemTitle = playlistItemTitle
} else {
playlistDialog.titleJobs.push(playlistItemFilename)
}
if (typeof playlistItemFilename !== "undefined") {
itemText += '<b>Filename:</b> ' + 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
}
}

View file

@ -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")
}
}
}

View file

@ -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())
}
}

View file

@ -27,6 +27,8 @@
<file alias="ChapterMarkerItem.qml">Items/ChapterMarkerItem.qml</file>
<file alias="TrackItem.qml">Items/TrackItem.qml</file>
<file alias="AudioDeviceItem.qml">Items/AudioDeviceItem.qml</file>
<file alias="ThumbnailProcess.qml">Items/ThumbnailProcess.qml</file>
<file alias="TitleProcess.qml">Items/TitleProcess.qml</file>
<file alias="CustomMenuItem.qml">Items/CustomMenuItem.qml</file>
<file alias="PlaylistDialog.qml">Dialogs/PlaylistDialog.qml</file>
<file>icons/YouTube/play.svg</file>

View file

@ -3,11 +3,14 @@
#include <QApplication>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QProcessEnvironment>
#include <QQmlApplicationEngine>
#include <QSequentialIterable>
#include <QString>
#include <QVariant>
#include <QtCore>
#include <QtNetwork>
#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);

View file

@ -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

77
tmp Normal file
View file

@ -0,0 +1,77 @@
#include "ThumbnailCache.h"
#include <QCryptographicHash>
#include <QPixmap>
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<int, QProcess::ExitStatus>::of(&QProcess::finished),
[=](int exitCode, QProcess::ExitStatus exitStatus){ /* ... */ });
connect(thumbnailerProcess, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
[=] (int exitCode, QProcess::ExitStatus exitStatus)
{
qDebug() << "finished. Exit code: " + exitCode ;
});
/*
connect(thumbnailerProcess,
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(
&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!";
}
}