Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae78302af | ||
|
|
7ec01c5854 | ||
|
|
2ae7ce866a |
No files matched your search
@@ -40,6 +40,7 @@ find_package(Ytdlp REQUIRED RUNTIME)
|
|||||||
find_package(YTMusicAPI REQUIRED RUNTIME)
|
find_package(YTMusicAPI REQUIRED RUNTIME)
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
add_subdirectory(qpersistentdata)
|
||||||
|
|
||||||
install(PROGRAMS org.kde.audiotube.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
install(PROGRAMS org.kde.audiotube.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||||
install(FILES org.kde.audiotube.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
install(FILES org.kde.audiotube.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||||
|
|||||||
@@ -7,23 +7,33 @@
|
|||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/8e/10/7b43c0a779d4c6331088d64af7e4a2da27b47817f558e97ebbadfc41869f/yt_dlp-2022.1.21-py2.py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl",
|
||||||
"sha256": "182903b78a4645edd504dcd9f2603f76f9fdaf8bd279fd45d01992f7637b6d8a"
|
"sha256": "d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/f6/06/e2ad9e93210790be86d36c6e2d5524ba54928c3ed27dd0be9b2ced7c57f1/pycryptodomex-3.12.0.zip",
|
"url": "https://files.pythonhosted.org/packages/f8/a3/622d9acbfb9a71144b5d7609906bc648c62e3ca5fdbb1c8cca222949d82c/websockets-10.3.tar.gz",
|
||||||
"sha256": "922e9dac0166e4617e5c7980d2cff6912a6eb5cb5c13e7ece222438650bd7f66"
|
"sha256": "fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"url": "https://files.pythonhosted.org/packages/69/77/591bbc51a5ed6a906a7813e60a9627f988f9546513fcf9d250eb31ec8689/websockets-10.1.tar.gz",
|
|
||||||
"sha256": "181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/16/b3/f7aa8edf2ff4495116f95fd442b2a346aa55d1d46313143c8814886dbcdb/mutagen-1.45.1-py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/16/b3/f7aa8edf2ff4495116f95fd442b2a346aa55d1d46313143c8814886dbcdb/mutagen-1.45.1-py3-none-any.whl",
|
||||||
"sha256": "9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"
|
"sha256": "9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/6e/77/efb8398b88e51b5d81ec5fefe1c3ea69e775a6f8fef06654be840d22b433/yt_dlp-2022.4.8-py2.py3-none-any.whl",
|
||||||
|
"sha256": "6edefe326b1e1478fdbe627a66203e5248a6b0dd50c101e682cf700ab70cdf72"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/2a/18/70c32fe9357f3eea18598b23aa9ed29b1711c3001835f7cf99a9818985d0/Brotli-1.0.9.zip",
|
||||||
|
"sha256": "4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/24/40/e249ac3845a2333ce50f1bb02299ffb766babdfe80ca9d31e0158ad06afd/pycryptodomex-3.14.1.tar.gz",
|
||||||
|
"sha256": "2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/33/d9/d18f3827dfd54acc48f29337059197514ad656ffc88e02a51e0457c5117a/ytmusicapi-0.20.0-py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/cb/58/834a6445479c72544d9285badce0a58650520c8ba5adfae7b98e61d133bb/ytmusicapi-0.21.0-py3-none-any.whl",
|
||||||
"sha256": "abdb90426baa14a7929ec810d65c1df228661b61d580678fc48c087909a9aea2"
|
"sha256": "29d8d44e1911458041882a453b05c37021ff850757c648f93a45d1775dddbae2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
@@ -17,8 +17,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/84/3e/1037abe6498e65d645ce7a22d3402605d49a3b2c7f20c3abb027760da4f0/charset_normalizer-2.0.10-py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl",
|
||||||
"sha256": "cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
|
"sha256": "44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl",
|
||||||
|
"sha256": "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
@@ -29,11 +34,6 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
|
||||||
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
|
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"url": "https://files.pythonhosted.org/packages/4e/b8/f5a25b22e803f0578e668daa33ba3701bb37858ec80e08a150bd7d2cf1b1/urllib3-1.26.8-py2.py3-none-any.whl",
|
|
||||||
"sha256": "000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||||
|
|
||||||
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
|
||||||
|
find_package(LMDB REQUIRED)
|
||||||
|
find_package(Qt5 COMPONENTS Core REQUIRED)
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Try to find the LMBD libraries and headers
|
||||||
|
# LMDB_FOUND - system has LMDB lib
|
||||||
|
# LMDB_INCLUDE_DIR - the LMDB include directory
|
||||||
|
# LMDB_LIBRARIES - Libraries needed to use LMDB
|
||||||
|
|
||||||
|
# FindCWD based on FindGMP by:
|
||||||
|
# Copyright (c) 2006, Laurent Montel, <[email protected]>
|
||||||
|
#
|
||||||
|
# Redistribution and use is allowed according to the terms of the BSD license.
|
||||||
|
|
||||||
|
# Adapted from FindCWD by:
|
||||||
|
# Copyright 2013 Conrad Steenberg <[email protected]>
|
||||||
|
# Aug 31, 2013
|
||||||
|
|
||||||
|
find_path(LMDB_INCLUDE_DIR NAMES lmdb.h PATHS "$ENV{LMDB_DIR}/include")
|
||||||
|
find_library(LMDB_LIBRARIES NAMES lmdb PATHS "$ENV{LMDB_DIR}/lib" )
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(LMDB DEFAULT_MSG LMDB_INCLUDE_DIR LMDB_LIBRARIES)
|
||||||
|
|
||||||
|
if(LMDB_FOUND)
|
||||||
|
message(STATUS "Found lmdb (include: ${LMDB_INCLUDE_DIR}, library: ${LMDB_LIBRARIES})")
|
||||||
|
mark_as_advanced(LMDB_INCLUDE_DIR LMDB_LIBRARIES)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
add_library(qpersistentdata STATIC qpersistentdata.h qpersistentdata.cpp)
|
||||||
|
target_link_libraries(qpersistentdata PUBLIC Qt::Core ${LMDB_LIBRARIES})
|
||||||
|
target_include_directories(qpersistentdata PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include "qpersistentdata.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto data = QPersistentData("/tmp");
|
||||||
|
Q_ASSERT(data.isValid());
|
||||||
|
|
||||||
|
data.insert(1, QSize(1,11));
|
||||||
|
Q_ASSERT(data.value<QSize>(1) == QSize(1,11));
|
||||||
|
data.insert("idList", QVector<int> {1, 2, 3, 4, 5});
|
||||||
|
data.insert(u"moinLeude", u"Moin Leude");
|
||||||
|
data.insert(QVector<QByteArray>({"moin", "leude"}), "Yo");
|
||||||
|
data.insert(QDateTime(), u"Hello World"_qs);
|
||||||
|
qDebug() << data.value<QVector<int>>("idList");
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#include "qpersistentdata.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include <optional>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
#include <lmdb.h>
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(PersistentDataLog)
|
||||||
|
Q_LOGGING_CATEGORY(PersistentDataLog, "qpersistentdata")
|
||||||
|
|
||||||
|
struct EnvironmentHolder {
|
||||||
|
MDB_env *env = nullptr;
|
||||||
|
int users = 0;
|
||||||
|
int maxDbs = 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
EnvironmentHolder DATABASE_ENVIRONMENT;
|
||||||
|
|
||||||
|
void qPersistentDataSetMaxDbs(int num)
|
||||||
|
{
|
||||||
|
DATABASE_ENVIRONMENT.maxDbs = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Increments the number of references to the global database
|
||||||
|
/// environment on construction, and decrements it on destruction
|
||||||
|
///
|
||||||
|
/// This should ensure that an EnvHandle is always a valid handle to
|
||||||
|
/// an MDB_env
|
||||||
|
///
|
||||||
|
class EnvHandle {
|
||||||
|
public:
|
||||||
|
EnvHandle() {
|
||||||
|
DATABASE_ENVIRONMENT.users++;
|
||||||
|
|
||||||
|
if (!DATABASE_ENVIRONMENT.env) {
|
||||||
|
const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % "/persistent-data/";
|
||||||
|
QDir(path).mkpath(QStringLiteral("."));
|
||||||
|
|
||||||
|
int s = mdb_env_create(&DATABASE_ENVIRONMENT.env);
|
||||||
|
Q_ASSERT(s == MDB_SUCCESS);
|
||||||
|
s = mdb_env_set_maxdbs(DATABASE_ENVIRONMENT.env, DATABASE_ENVIRONMENT.maxDbs);
|
||||||
|
Q_ASSERT(s == MDB_SUCCESS);
|
||||||
|
if (int status = mdb_env_open(DATABASE_ENVIRONMENT.env, path.toUtf8(), 0, 0664) != MDB_SUCCESS) {
|
||||||
|
qCDebug(PersistentDataLog) << "Failed to open database with exit code" << status;
|
||||||
|
mdb_env_close(DATABASE_ENVIRONMENT.env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvHandle(EnvHandle &) {
|
||||||
|
DATABASE_ENVIRONMENT.users++;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvHandle &operator=(const EnvHandle &) = default;
|
||||||
|
EnvHandle(EnvHandle &&) = default;
|
||||||
|
EnvHandle &operator=(EnvHandle &&) = default;
|
||||||
|
|
||||||
|
~EnvHandle() {
|
||||||
|
DATABASE_ENVIRONMENT.users--;
|
||||||
|
|
||||||
|
if (DATABASE_ENVIRONMENT.users == 0) {
|
||||||
|
qCDebug(PersistentDataLog) << "Closing database handle";
|
||||||
|
mdb_env_close(DATABASE_ENVIRONMENT.env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator MDB_env*() {
|
||||||
|
return DATABASE_ENVIRONMENT.env;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Qt wrapper for MDB_val
|
||||||
|
struct LMDBValueView : public MDB_val {
|
||||||
|
constexpr LMDBValueView() : MDB_val()
|
||||||
|
{
|
||||||
|
mv_size = 0;
|
||||||
|
mv_data = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr LMDBValueView(const QByteArray &bytes) : MDB_val()
|
||||||
|
{
|
||||||
|
mv_size = static_cast<size_t>(bytes.size());
|
||||||
|
mv_data = const_cast<char *>(bytes.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const char *data() const {
|
||||||
|
return static_cast<const char *>(mv_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr std::size_t size() const {
|
||||||
|
return mv_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QByteArray toByteArray() const {
|
||||||
|
return QByteArray(data(), size());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct QPersistentDataPrivate {
|
||||||
|
QByteArray name;
|
||||||
|
EnvHandle handle;
|
||||||
|
|
||||||
|
~QPersistentDataPrivate() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
QPersistentData::QPersistentData(EnvHandle handle, const QByteArray &name)
|
||||||
|
: d(std::make_unique<QPersistentDataPrivate>())
|
||||||
|
{
|
||||||
|
d->handle = handle;
|
||||||
|
d->name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPersistentData::QPersistentData(const QByteArray &name)
|
||||||
|
: QPersistentData(EnvHandle(), name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int QPersistentData::insert(const QByteArray &key, const QByteArray &value)
|
||||||
|
{
|
||||||
|
if (!d->handle) {
|
||||||
|
return MDB_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
MDB_txn *wtxn = nullptr;
|
||||||
|
if (int status = mdb_txn_begin(d->handle, nullptr, 0, &wtxn) != MDB_SUCCESS) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
MDB_dbi dbi {};
|
||||||
|
if (int status = mdb_dbi_open(wtxn, d->name, MDB_CREATE, &dbi) != MDB_SUCCESS) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMDBValueView k(key);
|
||||||
|
LMDBValueView v(value);
|
||||||
|
|
||||||
|
if (int status = mdb_put(wtxn, dbi, &k, &v, 0) != MDB_SUCCESS) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (int status = mdb_txn_commit(wtxn) != MDB_SUCCESS) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
mdb_dbi_close(d->handle, dbi);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray QPersistentData::value(const QByteArray &key) const
|
||||||
|
{
|
||||||
|
if (!d->handle) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
MDB_txn *rtxn = nullptr;
|
||||||
|
if (int status = mdb_txn_begin(d->handle, nullptr, MDB_RDONLY, &rtxn) != MDB_SUCCESS) {
|
||||||
|
qCDebug(PersistentDataLog) << "Failed to start transaction:" << status;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
MDB_dbi dbi {};
|
||||||
|
if (int status = mdb_dbi_open(rtxn, d->name, MDB_CREATE, &dbi) != MDB_SUCCESS) {
|
||||||
|
qCDebug(PersistentDataLog) << "Failed to open database handle:" << status;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
LMDBValueView k(key);
|
||||||
|
LMDBValueView v;
|
||||||
|
|
||||||
|
if (int status = mdb_get(rtxn, dbi, &k, &v) != MDB_SUCCESS) {
|
||||||
|
qCDebug(PersistentDataLog) << "Failed to retrieve data from the database:" << status;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
mdb_txn_abort(rtxn);
|
||||||
|
mdb_dbi_close(d->handle, dbi);
|
||||||
|
|
||||||
|
return v.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QPersistentData::isValid() const
|
||||||
|
{
|
||||||
|
return d->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPersistentData::~QPersistentData() = default;
|
||||||
|
QPersistentData::QPersistentData(QPersistentData &&other) noexcept = default;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct QPersistentDataPrivate;
|
||||||
|
class MDB_env;
|
||||||
|
class MDB_cursor;
|
||||||
|
class MDB_txn;
|
||||||
|
class EnvHandle;
|
||||||
|
typedef unsigned int MDB_dbi;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Set the number of databases that can be used in parallel
|
||||||
|
///
|
||||||
|
void qPersistentDataSetMaxDbs(int num);
|
||||||
|
|
||||||
|
class QPersistentData {
|
||||||
|
public:
|
||||||
|
QPersistentData(QPersistentData &&other) noexcept;
|
||||||
|
~QPersistentData();
|
||||||
|
|
||||||
|
QPersistentData(const QByteArray &name);
|
||||||
|
|
||||||
|
int insert(const QByteArray &key, const QByteArray &value);
|
||||||
|
|
||||||
|
template <typename KeyType, typename ValueType>
|
||||||
|
int insert(const KeyType &key, const ValueType &value) {
|
||||||
|
QByteArray keyS;
|
||||||
|
QByteArray valS;
|
||||||
|
|
||||||
|
QDataStream keyStream(&keyS, QIODevice::WriteOnly);
|
||||||
|
keyStream << key;
|
||||||
|
QDataStream valStream(&valS, QIODevice::WriteOnly);
|
||||||
|
valStream << value;
|
||||||
|
|
||||||
|
return insert(keyS, valS);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QByteArray value(const QByteArray &key) const;
|
||||||
|
|
||||||
|
template <typename ValueType, typename KeyType>
|
||||||
|
ValueType value(const KeyType &key) const {
|
||||||
|
QByteArray keyS;
|
||||||
|
QDataStream keyStream(&keyS, QIODevice::WriteOnly);
|
||||||
|
keyStream << key;
|
||||||
|
|
||||||
|
auto val = value(keyS);
|
||||||
|
QDataStream vStream(val);
|
||||||
|
|
||||||
|
ValueType out;
|
||||||
|
vStream >> out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool isValid() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit QPersistentData(EnvHandle handle, const QByteArray &name);
|
||||||
|
std::shared_ptr<QPersistentDataPrivate> d;
|
||||||
|
};
|
||||||
+1
-1
@@ -3,5 +3,5 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
ytmusicapi==0.20.0
|
ytmusicapi==0.21.0
|
||||||
yt-dlp
|
yt-dlp
|
||||||
@@ -26,6 +26,7 @@ add_executable(audiotube
|
|||||||
playerutils.cpp
|
playerutils.cpp
|
||||||
abstractytmusicmodel.cpp
|
abstractytmusicmodel.cpp
|
||||||
multiiterableview.h
|
multiiterableview.h
|
||||||
|
library.cpp
|
||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ target_link_libraries(audiotube
|
|||||||
KF5::CoreAddons
|
KF5::CoreAddons
|
||||||
KF5::Crash
|
KF5::Crash
|
||||||
ytm
|
ytm
|
||||||
|
qpersistentdata
|
||||||
)
|
)
|
||||||
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING)
|
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING)
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -11,21 +11,22 @@ AlbumModel::AlbumModel(QObject *parent)
|
|||||||
{
|
{
|
||||||
connect(this, &AlbumModel::browseIdChanged, this, [this] {
|
connect(this, &AlbumModel::browseIdChanged, this, [this] {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
YTMusicThread::instance()->fetchAlbum(m_browseId);
|
auto future = YTMusicThread::instance()->fetchAlbum(m_browseId);
|
||||||
});
|
connectFuture(future, this, [=, this](const album::Album &album) {
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [this](const album::Album &album) {
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_album = album;
|
m_album = album;
|
||||||
endResetModel();
|
endResetModel();
|
||||||
std::sort(m_album.thumbnails.begin(), m_album.thumbnails.end());
|
std::sort(m_album.thumbnails.begin(), m_album.thumbnails.end());
|
||||||
|
|
||||||
Q_EMIT titleChanged();
|
Q_EMIT titleChanged();
|
||||||
Q_EMIT thumbnailUrlChanged();
|
Q_EMIT thumbnailUrlChanged();
|
||||||
Q_EMIT playlistIdChanged();
|
Q_EMIT playlistIdChanged();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
|
||||||
|
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=, this] {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -17,26 +17,26 @@ ArtistModel::ArtistModel(QObject *parent)
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
YTMusicThread::instance()->fetchArtist(m_channelId);
|
auto future = YTMusicThread::instance()->fetchArtist(m_channelId);
|
||||||
});
|
connectFuture(future, this, [=, this](const artist::Artist &artist) {
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [this](const artist::Artist &artist) {
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_artist = artist;
|
m_artist = artist;
|
||||||
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
|
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
|
||||||
|
|
||||||
albums = m_artist.albums ? m_artist.albums->results : std::vector<artist::Artist::Album>();
|
albums = m_artist.albums ? m_artist.albums->results : std::vector<artist::Artist::Album>();
|
||||||
singles = m_artist.singles ? m_artist.singles->results : std::vector<artist::Artist::Single>();
|
singles = m_artist.singles ? m_artist.singles->results : std::vector<artist::Artist::Single>();
|
||||||
songs = m_artist.songs ? m_artist.songs->results : std::vector<artist::Artist::Song>();
|
songs = m_artist.songs ? m_artist.songs->results : std::vector<artist::Artist::Song>();
|
||||||
videos = m_artist.videos ? m_artist.videos->results : std::vector<artist::Artist::Video>();
|
videos = m_artist.videos ? m_artist.videos->results : std::vector<artist::Artist::Video>();
|
||||||
|
|
||||||
// std::span can't know if the data pointer underneath it was changed, so re-create
|
// std::span can't know if the data pointer underneath it was changed, so re-create
|
||||||
m_view = MultiIterableView(albums, singles, songs, videos);
|
m_view = MultiIterableView(albums, singles, songs, videos);
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
|
||||||
Q_EMIT titleChanged();
|
Q_EMIT titleChanged();
|
||||||
Q_EMIT thumbnailUrlChanged();
|
Q_EMIT thumbnailUrlChanged();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
+35
-38
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QFutureInterface>
|
||||||
|
|
||||||
#include <pybind11/embed.h>
|
#include <pybind11/embed.h>
|
||||||
|
|
||||||
@@ -13,6 +16,13 @@
|
|||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <ranges>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
template <typename R, typename T, typename OP>
|
template <typename R, typename T, typename OP>
|
||||||
std::optional<R> map_optional(const std::optional<T> &optional, OP op) {
|
std::optional<R> map_optional(const std::optional<T> &optional, OP op) {
|
||||||
if (optional.has_value()) {
|
if (optional.has_value()) {
|
||||||
@@ -49,95 +59,82 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
|
|||||||
//
|
//
|
||||||
// search
|
// search
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::search(const QString &query)
|
QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QString &query)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<std::vector<search::SearchResultItem>>([=, this]() {
|
||||||
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
|
return m_ytm->search(query.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchArtist
|
// fetchArtist
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchArtist(const QString &channelId)
|
QFuture<artist::Artist> AsyncYTMusic::fetchArtist(const QString &channelId)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<artist::Artist>([=, this]() {
|
||||||
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
|
return m_ytm->get_artist(channelId.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchAlbum
|
// fetchAlbum
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchAlbum(const QString &browseId)
|
QFuture<album::Album> AsyncYTMusic::fetchAlbum(const QString &browseId)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<album::Album>([=, this]() {
|
||||||
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
|
return m_ytm->get_album(browseId.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchSong
|
// fetchSong
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchSong(const QString &videoId)
|
QFuture<std::optional<song::Song>> AsyncYTMusic::fetchSong(const QString &videoId)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<std::optional<song::Song>>([=, this]() {
|
||||||
auto maybeSong = m_ytm->get_song(videoId.toStdString());
|
return m_ytm->get_song(videoId.toStdString());
|
||||||
if (maybeSong.has_value()) {
|
|
||||||
Q_EMIT fetchSongFinished(*maybeSong);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchPlaylist
|
// fetchPlaylist
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
|
QFuture<playlist::Playlist> AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<playlist::Playlist>([=, this]() {
|
||||||
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
|
return m_ytm->get_playlist(playlistId.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchArtistAlbum
|
// fetchArtistAlbum
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString ¶ms)
|
QFuture<std::vector<artist::Artist::Album>> AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString ¶ms)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<std::vector<artist::Artist::Album>>([=, this]() {
|
||||||
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString()));
|
return m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// extractVideoInfo
|
// extractVideoInfo
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::extractVideoInfo(const QString &videoId)
|
QFuture<video_info::VideoInfo> AsyncYTMusic::extractVideoInfo(const QString &videoId)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<video_info::VideoInfo>([=, this]() {
|
||||||
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
|
return m_ytm->extract_video_info(videoId.toStdString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// fetchWatchPlaylist
|
// fetchWatchPlaylist
|
||||||
//
|
//
|
||||||
void AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
|
QFuture<watch::Playlist> AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
|
||||||
{
|
{
|
||||||
invokeAndCatchOnThread([=, this]() {
|
return invokeAndCatchOnThread<watch::Playlist>([=, this]() {
|
||||||
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
|
return m_ytm->get_watch_playlist(
|
||||||
map_optional<std::string>(videoId, &QString::toStdString),
|
map_optional<std::string>(videoId, &QString::toStdString),
|
||||||
map_optional<std::string>(playlistId, &QString::toStdString)
|
map_optional<std::string>(playlistId, &QString::toStdString)
|
||||||
));
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void AsyncYTMusic::invokeAndCatchOnThread(const std::function<void()> &fun) {
|
|
||||||
QMetaObject::invokeMethod(this, [=, this]() {
|
|
||||||
try {
|
|
||||||
fun();
|
|
||||||
} catch (const py::error_already_set &err) {
|
|
||||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-17
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -45,6 +47,17 @@ private:
|
|||||||
std::unique_ptr<T> m_item = nullptr;
|
std::unique_ptr<T> m_item = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T, typename Function>
|
||||||
|
void connectFuture(const QFuture<T> &future, QObject *self, const Function &fun) {
|
||||||
|
auto watcher = std::make_shared<QFutureWatcher<T>>();
|
||||||
|
watcher->setFuture(future);
|
||||||
|
QObject::connect(watcher.get(), &QFutureWatcherBase::finished, self, [watcher, fun, future] {
|
||||||
|
if (future.resultCount() > 0) {
|
||||||
|
fun(watcher->result());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class AsyncYTMusic : public QObject
|
class AsyncYTMusic : public QObject
|
||||||
{
|
{
|
||||||
friend class YTMusicThread;
|
friend class YTMusicThread;
|
||||||
@@ -53,30 +66,22 @@ class AsyncYTMusic : public QObject
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
// public functions need to be thread safe
|
// public functions need to be thread safe
|
||||||
void search(const QString &query);
|
QFuture<std::vector<search::SearchResultItem>> search(const QString &query);
|
||||||
Q_SIGNAL void searchFinished(std::vector<search::SearchResultItem>);
|
|
||||||
|
|
||||||
void fetchArtist(const QString &channelId);
|
QFuture<artist::Artist> fetchArtist(const QString &channelId);
|
||||||
Q_SIGNAL void fetchArtistFinished(artist::Artist);
|
|
||||||
|
|
||||||
void fetchAlbum(const QString &browseId);
|
QFuture<album::Album> fetchAlbum(const QString &browseId);
|
||||||
Q_SIGNAL void fetchAlbumFinished(album::Album);
|
|
||||||
|
|
||||||
void fetchSong(const QString &videoId);
|
QFuture<std::optional<song::Song> > fetchSong(const QString &videoId);
|
||||||
Q_SIGNAL void fetchSongFinished(song::Song);
|
|
||||||
|
|
||||||
void fetchPlaylist(const QString &playlistId);
|
QFuture<playlist::Playlist> fetchPlaylist(const QString &playlistId);
|
||||||
Q_SIGNAL void fetchPlaylistFinished(playlist::Playlist);
|
|
||||||
|
|
||||||
void fetchArtistAlbums(const QString &channelId, const QString ¶ms);
|
QFuture<std::vector<artist::Artist::Album>> fetchArtistAlbums(const QString &channelId, const QString ¶ms);
|
||||||
Q_SIGNAL void fetchArtistAlbumsFinished(std::vector<artist::Artist::Album>);
|
|
||||||
|
|
||||||
void extractVideoInfo(const QString &videoId);
|
QFuture<video_info::VideoInfo> extractVideoInfo(const QString &videoId);
|
||||||
Q_SIGNAL void extractVideoInfoFinished(video_info::VideoInfo);
|
|
||||||
|
|
||||||
void fetchWatchPlaylist(const std::optional<QString> &videoId = std::nullopt ,
|
QFuture<watch::Playlist> fetchWatchPlaylist(const std::optional<QString> &videoId = std::nullopt ,
|
||||||
const std::optional<QString> &playlistId = std::nullopt);
|
const std::optional<QString> &playlistId = std::nullopt);
|
||||||
Q_SIGNAL void fetchWatchPlaylistFinished(watch::Playlist);
|
|
||||||
|
|
||||||
Q_SIGNAL void errorOccurred(const QString &error);
|
Q_SIGNAL void errorOccurred(const QString &error);
|
||||||
|
|
||||||
@@ -85,7 +90,21 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
/// Invokes the given function on the thread of the YTMusic object, and handles exceptions that occur while invoking it.
|
/// Invokes the given function on the thread of the YTMusic object, and handles exceptions that occur while invoking it.
|
||||||
void invokeAndCatchOnThread(const std::function<void()> &fun);
|
template <typename ReturnType>
|
||||||
|
QFuture<ReturnType> invokeAndCatchOnThread(const std::function<ReturnType()> &fun) {
|
||||||
|
auto interface = std::make_shared<QFutureInterface<ReturnType>>();
|
||||||
|
QMetaObject::invokeMethod(this, [=, this]() {
|
||||||
|
try {
|
||||||
|
ReturnType val = fun();
|
||||||
|
interface->reportResult(val);
|
||||||
|
interface->reportFinished();
|
||||||
|
} catch (const std::exception &err) {
|
||||||
|
interface->reportFinished();
|
||||||
|
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return interface->future();
|
||||||
|
}
|
||||||
|
|
||||||
// Python interpreter will be initialized from the thread calling the methods
|
// Python interpreter will be initialized from the thread calling the methods
|
||||||
Lazy<YTMusic> m_ytm;
|
Lazy<YTMusic> m_ytm;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import QtQuick 2.1
|
||||||
|
import QtQuick.Controls 2.12 as Controls
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import org.kde.kirigami 2.14 as Kirigami
|
||||||
|
|
||||||
|
import org.kde.ytmusic 1.0
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
ColumnLayout {
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: i18n("Favourites")
|
||||||
|
}
|
||||||
|
Controls.ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
RowLayout {
|
||||||
|
Repeater {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: Library.favourites
|
||||||
|
onCountChanged: () => console.log("Count:", count)
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
Layout.fillWidth: false
|
||||||
|
Layout.maximumWidth: 200
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log(modelData)
|
||||||
|
}
|
||||||
|
Kirigami.Card {
|
||||||
|
id: card
|
||||||
|
Layout.preferredHeight: 200
|
||||||
|
|
||||||
|
onClicked: play(modelData)
|
||||||
|
|
||||||
|
ThumbnailSource {
|
||||||
|
id: thumbnailSource
|
||||||
|
videoId: modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
header: Image {
|
||||||
|
sourceSize: "200x200"
|
||||||
|
source: thumbnailSource.cachedPath
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Controls.Label {
|
||||||
|
Layout.maximumWidth: 200
|
||||||
|
text: Library.videoTitle(modelData)
|
||||||
|
elide: Qt.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,15 +38,18 @@ Item {
|
|||||||
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
|
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
|
||||||
visible: footerItem.maximized
|
visible: footerItem.maximized
|
||||||
|
|
||||||
Item {
|
ColumnLayout {
|
||||||
Layout.fillWidth: info.thumbnail
|
Layout.fillWidth: info.thumbnail
|
||||||
Layout.fillHeight: info.thumbnail
|
Layout.fillHeight: info.thumbnail
|
||||||
Image {
|
Image {
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
source: info.thumbnail
|
source: info.thumbnail
|
||||||
|
Layout.preferredWidth: footerLayout.width * 0.5
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
}
|
}
|
||||||
|
Controls.ToolButton{
|
||||||
|
text: "Fav"
|
||||||
|
onClicked: Library.addFavourite(info.videoId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -36,10 +36,49 @@ Kirigami.ApplicationWindow {
|
|||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumWidth: 400
|
Layout.maximumWidth: 400
|
||||||
|
onFocusChanged: {
|
||||||
|
if (focus)
|
||||||
|
popup.open()
|
||||||
|
}
|
||||||
|
onTextChanged: {
|
||||||
|
if (!popup.opened) {
|
||||||
|
popup.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Controls.Popup {
|
||||||
|
id: popup
|
||||||
|
x: searchField.y
|
||||||
|
y: searchField.y + searchField.height
|
||||||
|
visible: true
|
||||||
|
width: searchField.width
|
||||||
|
height: contentItem.implicitHeight
|
||||||
|
|
||||||
|
contentItem: Controls.ScrollView {
|
||||||
|
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
|
||||||
|
ListView {
|
||||||
|
id: completionList
|
||||||
|
model: Library.searches
|
||||||
|
delegate: Controls.ItemDelegate {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData
|
||||||
|
onClicked: {
|
||||||
|
searchField.text = modelData
|
||||||
|
searchField.accepted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
pageStack.clear()
|
pageStack.clear()
|
||||||
|
if (text) {
|
||||||
|
Library.addSearch(text)
|
||||||
|
}
|
||||||
pageStack.push("qrc:/SearchPage.qml", {
|
pageStack.push("qrc:/SearchPage.qml", {
|
||||||
"searchQuery": text})
|
"searchQuery": text})
|
||||||
|
popup.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item {
|
Item {
|
||||||
@@ -56,7 +95,7 @@ Kirigami.ApplicationWindow {
|
|||||||
id: contextDrawer
|
id: contextDrawer
|
||||||
}
|
}
|
||||||
|
|
||||||
pageStack.initialPage: "qrc:/SearchPage.qml"
|
pageStack.initialPage: "qrc:/LibraryPage.qml"
|
||||||
|
|
||||||
function play(videoId) {
|
function play(videoId) {
|
||||||
UserPlaylistModel.initialVideoId = videoId
|
UserPlaylistModel.initialVideoId = videoId
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
#include "library.h"
|
||||||
|
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
|
||||||
|
#include <qpersistentdata.h>
|
||||||
|
|
||||||
|
Library::Library(QObject *parent)
|
||||||
|
: QObject{parent}
|
||||||
|
, m_storage("library")
|
||||||
|
, m_cachedVideoTitles("videoTitles")
|
||||||
|
{
|
||||||
|
connect(this, &Library::favouritesChanged, this, []() {
|
||||||
|
qDebug() << "Favs changed" ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Library &Library::instance()
|
||||||
|
{
|
||||||
|
static Library inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Library::favourites() const
|
||||||
|
{
|
||||||
|
return m_storage.value<QStringList>("favourites");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Library::addFavourite(const QString &videoId)
|
||||||
|
{
|
||||||
|
auto favs = favourites();
|
||||||
|
favs.push_front(videoId);
|
||||||
|
m_storage.insert("favourites", favs);
|
||||||
|
Q_EMIT favouritesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Library::searches() const
|
||||||
|
{
|
||||||
|
auto s = m_storage.value<QStringList>("searches");
|
||||||
|
qDebug() << "searches" << s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Library::addSearch(const QString &text)
|
||||||
|
{
|
||||||
|
m_storage.insert("searches", searches() << text);
|
||||||
|
Q_EMIT searchesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Library::playbackHistory() const
|
||||||
|
{
|
||||||
|
auto h = m_storage.value<QStringList>("playbackHistory");
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Library::addPlaybackHistoryItem(const QString &videoId)
|
||||||
|
{
|
||||||
|
m_storage.insert("playbackHistory", playbackHistory() << videoId);
|
||||||
|
Q_EMIT playbackHistoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Library::videoTitle(const QString &videoId)
|
||||||
|
{
|
||||||
|
qDebug() << "Fetching video title" << videoId;
|
||||||
|
auto t = m_cachedVideoTitles.value<QString>(videoId);
|
||||||
|
qDebug() << "title" << t;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Library::addVideoTitle(const QString &videoId, const QString &title)
|
||||||
|
{
|
||||||
|
m_cachedVideoTitles.insert(videoId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkAccessManager &Library::nam()
|
||||||
|
{
|
||||||
|
return m_networkImageCacher;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThumbnailSource::setVideoId(const QString &id) {
|
||||||
|
if (m_videoId == id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_videoId = id;
|
||||||
|
Q_EMIT videoIdChanged();
|
||||||
|
|
||||||
|
const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % QDir::separator() % "thumbnails";
|
||||||
|
QDir(cacheDir).mkpath(QStringLiteral("."));
|
||||||
|
const QString cacheLocation = cacheDir % QDir::separator() % id % ".webp";
|
||||||
|
|
||||||
|
if (QFile::exists(cacheLocation)) {
|
||||||
|
setCachedPath(QUrl::fromLocalFile(cacheLocation));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto future = YTMusicThread::instance()->extractVideoInfo(id);
|
||||||
|
connectFuture(future, this, [this, id, cacheLocation](video_info::VideoInfo &&info) {
|
||||||
|
auto *reply = Library::instance().nam().get(QNetworkRequest(QUrl(QString::fromStdString(info.thumbnail))));
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [id, reply, this, cacheLocation]() {
|
||||||
|
QFile file(cacheLocation);
|
||||||
|
file.open(QFile::WriteOnly);
|
||||||
|
file.write(reply->readAll());
|
||||||
|
setCachedPath(QUrl::fromLocalFile(cacheLocation));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQuickAsyncImageProvider>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <qpersistentdata.h>
|
||||||
|
|
||||||
|
#include "asyncytmusic.h"
|
||||||
|
|
||||||
|
class Library : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QStringList favourites READ favourites NOTIFY favouritesChanged)
|
||||||
|
Q_PROPERTY(QStringList searches READ searches NOTIFY searchesChanged)
|
||||||
|
Q_PROPERTY(QStringList playbackHistory READ playbackHistory NOTIFY playbackHistoryChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Library(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
static Library &instance();
|
||||||
|
|
||||||
|
QStringList favourites() const;
|
||||||
|
Q_SIGNAL void favouritesChanged();
|
||||||
|
Q_INVOKABLE void addFavourite(const QString &videoId);
|
||||||
|
|
||||||
|
QStringList searches() const;
|
||||||
|
Q_SIGNAL void searchesChanged();
|
||||||
|
Q_INVOKABLE void addSearch(const QString &text);
|
||||||
|
|
||||||
|
QStringList playbackHistory() const;
|
||||||
|
Q_SIGNAL void playbackHistoryChanged();
|
||||||
|
Q_INVOKABLE void addPlaybackHistoryItem(const QString &videoId);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString videoTitle(const QString &videoId);
|
||||||
|
Q_INVOKABLE void addVideoTitle(const QString &videoId, const QString &title);
|
||||||
|
|
||||||
|
QNetworkAccessManager &nam();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkAccessManager m_networkImageCacher;
|
||||||
|
QPersistentData m_storage;
|
||||||
|
QPersistentData m_cachedVideoTitles;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbnailSource : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString videoId READ videoId WRITE setVideoId NOTIFY videoIdChanged)
|
||||||
|
Q_PROPERTY(QUrl cachedPath READ cachedPath NOTIFY cachedPathChanged)
|
||||||
|
|
||||||
|
QString videoId() const {
|
||||||
|
return m_videoId;
|
||||||
|
}
|
||||||
|
void setVideoId(const QString &id);
|
||||||
|
Q_SIGNAL void videoIdChanged();
|
||||||
|
|
||||||
|
QUrl cachedPath() const {
|
||||||
|
return m_cachedPath;
|
||||||
|
}
|
||||||
|
void setCachedPath(const QUrl &path) {
|
||||||
|
m_cachedPath = path;
|
||||||
|
cachedPathChanged();
|
||||||
|
}
|
||||||
|
Q_SIGNAL void cachedPathChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_videoId;
|
||||||
|
QUrl m_cachedPath;
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "errorhandler.h"
|
#include "errorhandler.h"
|
||||||
#include "playlistmodel.h"
|
#include "playlistmodel.h"
|
||||||
#include "playerutils.h"
|
#include "playerutils.h"
|
||||||
|
#include "library.h"
|
||||||
|
|
||||||
constexpr auto URI = "org.kde.ytmusic";
|
constexpr auto URI = "org.kde.ytmusic";
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
|||||||
qmlRegisterSingletonType<PlayerUtils>(URI, 1, 0, "PlayerUtils", [](QQmlEngine *, QJSEngine *) {
|
qmlRegisterSingletonType<PlayerUtils>(URI, 1, 0, "PlayerUtils", [](QQmlEngine *, QJSEngine *) {
|
||||||
return new PlayerUtils();
|
return new PlayerUtils();
|
||||||
});
|
});
|
||||||
|
qmlRegisterSingletonType<Library>(URI, 1, 0, "Library", [](QQmlEngine *, QJSEngine *) {
|
||||||
|
return new Library();
|
||||||
|
});
|
||||||
|
qmlRegisterType<ThumbnailSource>(URI, 1, 0, "ThumbnailSource");
|
||||||
|
|
||||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||||
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
|
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
|
||||||
|
|||||||
+10
-10
@@ -15,17 +15,17 @@ PlaylistModel::PlaylistModel(QObject *parent)
|
|||||||
{
|
{
|
||||||
connect(this, &PlaylistModel::playlistIdChanged, this, [=, this] {
|
connect(this, &PlaylistModel::playlistIdChanged, this, [=, this] {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
YTMusicThread::instance()->fetchPlaylist(m_playlistId);
|
auto future = YTMusicThread::instance()->fetchPlaylist(m_playlistId);
|
||||||
});
|
connectFuture(future, this, [=, this](const playlist::Playlist &playlist) {
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [this](const playlist::Playlist &playlist) {
|
setLoading(false);
|
||||||
setLoading(false);
|
beginResetModel();
|
||||||
beginResetModel();
|
m_playlist = playlist;
|
||||||
m_playlist = playlist;
|
std::sort(m_playlist.thumbnails.begin(), m_playlist.thumbnails.end());
|
||||||
std::sort(m_playlist.thumbnails.begin(), m_playlist.thumbnails.end());
|
endResetModel();
|
||||||
endResetModel();
|
|
||||||
|
|
||||||
Q_EMIT titleChanged();
|
Q_EMIT titleChanged();
|
||||||
Q_EMIT thumbnailUrlChanged();
|
Q_EMIT thumbnailUrlChanged();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-6
@@ -1,9 +1,3 @@
|
|||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
||||||
-->
|
|
||||||
|
|
||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file alias="main.qml">contents/ui/main.qml</file>
|
<file alias="main.qml">contents/ui/main.qml</file>
|
||||||
@@ -13,5 +7,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
|||||||
<file alias="SearchPage.qml">contents/ui/SearchPage.qml</file>
|
<file alias="SearchPage.qml">contents/ui/SearchPage.qml</file>
|
||||||
<file alias="PlaylistPage.qml">contents/ui/PlaylistPage.qml</file>
|
<file alias="PlaylistPage.qml">contents/ui/PlaylistPage.qml</file>
|
||||||
<file alias="ConfirmationMessage.qml">contents/ui/ConfirmationMessage.qml</file>
|
<file alias="ConfirmationMessage.qml">contents/ui/ConfirmationMessage.qml</file>
|
||||||
|
<file alias="LibraryPage.qml">contents/ui/LibraryPage.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
+7
-11
@@ -16,17 +16,13 @@ SearchModel::SearchModel(QObject *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
YTMusicThread::instance()->search(m_searchQuery);
|
auto future = YTMusicThread::instance()->search(m_searchQuery);
|
||||||
});
|
connectFuture(future, this, [=, this](const std::vector<search::SearchResultItem> &results) {
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [this](const std::vector<search::SearchResultItem> &results) {
|
beginResetModel();
|
||||||
beginResetModel();
|
setLoading(false);
|
||||||
setLoading(false);
|
m_searchResults = results;
|
||||||
m_searchResults = results;
|
endResetModel();
|
||||||
std::sort(m_searchResults.begin(), m_searchResults.end(),
|
});
|
||||||
[](search::SearchResultItem const & a, search::SearchResultItem const & b) {
|
|
||||||
return SearchModel::itemType(a) < SearchModel::itemType(b);
|
|
||||||
});
|
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
+20
-17
@@ -9,29 +9,14 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
|
||||||
#include "playlistutils.h"
|
#include "playlistutils.h"
|
||||||
|
|
||||||
UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
||||||
: AbstractYTMusicModel(parent)
|
: AbstractYTMusicModel(parent)
|
||||||
{
|
{
|
||||||
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [this] {
|
auto handleResult = [=, this](const watch::Playlist &playlist) {
|
||||||
if (m_initialVideoId.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
|
|
||||||
});
|
|
||||||
connect(this, &UserPlaylistModel::playlistIdChanged, this, [this] {
|
|
||||||
if (m_playlistId.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
|
|
||||||
});
|
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [this](const watch::Playlist &playlist) {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
@@ -48,6 +33,24 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
|||||||
} else {
|
} else {
|
||||||
setCurrentVideoId({});
|
setCurrentVideoId({});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=, this] {
|
||||||
|
if (m_initialVideoId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
auto future = YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
|
||||||
|
connectFuture(future, this, handleResult);
|
||||||
|
});
|
||||||
|
connect(this, &UserPlaylistModel::playlistIdChanged, this, [=, this] {
|
||||||
|
if (m_playlistId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
auto future = YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
|
||||||
|
connectFuture(future, this, handleResult);;
|
||||||
});
|
});
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
+17
-10
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
#include "videoinfoextractor.h"
|
#include "videoinfoextractor.h"
|
||||||
|
|
||||||
#include <asyncytmusic.h>
|
#include <QFutureWatcher>
|
||||||
|
|
||||||
|
#include "asyncytmusic.h"
|
||||||
|
#include "library.h"
|
||||||
|
|
||||||
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -20,16 +23,17 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [this](const video_info::VideoInfo &info) {
|
auto future = YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
|
||||||
m_videoInfo = info;
|
connectFuture(future, this, [this](const video_info::VideoInfo &videoInfo) {
|
||||||
setLoading(false);
|
m_videoInfo = videoInfo;
|
||||||
Q_EMIT audioUrlChanged();
|
setLoading(false);
|
||||||
Q_EMIT titleChanged();
|
Library::instance().addVideoTitle(m_videoId, QString::fromStdString(m_videoInfo.title));
|
||||||
Q_EMIT songChanged();
|
Q_EMIT audioUrlChanged();
|
||||||
Q_EMIT thumbnailChanged();
|
Q_EMIT titleChanged();
|
||||||
|
Q_EMIT songChanged();
|
||||||
|
Q_EMIT thumbnailChanged();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +70,9 @@ QString VideoInfoExtractor::videoId() const
|
|||||||
|
|
||||||
void VideoInfoExtractor::setVideoId(const QString &videoId)
|
void VideoInfoExtractor::setVideoId(const QString &videoId)
|
||||||
{
|
{
|
||||||
|
if (m_videoId == videoId || videoId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_videoId = videoId;
|
m_videoId = videoId;
|
||||||
Q_EMIT videoIdChanged();
|
Q_EMIT videoIdChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user