Compare commits

..
Author SHA1 Message Date
Jonah Brüchert 5112d0bd1b MultiIterableView: Add convinience std::vector constructor 2022-02-14 17:58:55 +01:00
Dimitris Kardarakos 2b60ae8745 Power search page list sections
The Search Page list view can present content in sections according to media type (artist, playlist, song, video). For sections to work, the model that provides the search results should sort the results accordingly.
2022-02-14 17:25:41 +01:00
28 changed files with 161 additions and 791 deletions

No files matched your search

-1
View File
@@ -40,7 +40,6 @@ find_package(Ytdlp REQUIRED RUNTIME)
find_package(YTMusicAPI REQUIRED RUNTIME)
add_subdirectory(src)
add_subdirectory(qpersistentdata)
install(PROGRAMS org.kde.audiotube.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.audiotube.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
+9 -19
View File
@@ -7,33 +7,23 @@
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl",
"sha256": "d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
"url": "https://files.pythonhosted.org/packages/8e/10/7b43c0a779d4c6331088d64af7e4a2da27b47817f558e97ebbadfc41869f/yt_dlp-2022.1.21-py2.py3-none-any.whl",
"sha256": "182903b78a4645edd504dcd9f2603f76f9fdaf8bd279fd45d01992f7637b6d8a"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f8/a3/622d9acbfb9a71144b5d7609906bc648c62e3ca5fdbb1c8cca222949d82c/websockets-10.3.tar.gz",
"sha256": "fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"
"url": "https://files.pythonhosted.org/packages/f6/06/e2ad9e93210790be86d36c6e2d5524ba54928c3ed27dd0be9b2ced7c57f1/pycryptodomex-3.12.0.zip",
"sha256": "922e9dac0166e4617e5c7980d2cff6912a6eb5cb5c13e7ece222438650bd7f66"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/69/77/591bbc51a5ed6a906a7813e60a9627f988f9546513fcf9d250eb31ec8689/websockets-10.1.tar.gz",
"sha256": "181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/16/b3/f7aa8edf2ff4495116f95fd442b2a346aa55d1d46313143c8814886dbcdb/mutagen-1.45.1-py3-none-any.whl",
"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"
}
]
}
+9 -9
View File
@@ -7,8 +7,8 @@
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/cb/58/834a6445479c72544d9285badce0a58650520c8ba5adfae7b98e61d133bb/ytmusicapi-0.21.0-py3-none-any.whl",
"sha256": "29d8d44e1911458041882a453b05c37021ff850757c648f93a45d1775dddbae2"
"url": "https://files.pythonhosted.org/packages/33/d9/d18f3827dfd54acc48f29337059197514ad656ffc88e02a51e0457c5117a/ytmusicapi-0.20.0-py3-none-any.whl",
"sha256": "abdb90426baa14a7929ec810d65c1df228661b61d580678fc48c087909a9aea2"
},
{
"type": "file",
@@ -17,13 +17,8 @@
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl",
"sha256": "44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl",
"sha256": "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
"url": "https://files.pythonhosted.org/packages/84/3e/1037abe6498e65d645ce7a22d3402605d49a3b2c7f20c3abb027760da4f0/charset_normalizer-2.0.10-py3-none-any.whl",
"sha256": "cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
},
{
"type": "file",
@@ -34,6 +29,11 @@
"type": "file",
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/4e/b8/f5a25b22e803f0578e668daa33ba3701bb37858ec80e08a150bd7d2cf1b1/urllib3-1.26.8-py2.py3-none-any.whl",
"sha256": "000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"
}
]
}
-4
View File
@@ -31,7 +31,6 @@ SPDX-License-Identifier: CC0-1.0
<name xml:lang="sv">AudioTube</name>
<name xml:lang="uk">AudioTube</name>
<name xml:lang="x-test">xxAudioTubexx</name>
<name xml:lang="zh-CN">AudioTube</name>
<summary>Convergent YouTube Music client</summary>
<summary xml:lang="ar">عميل موسيقى يوتيوب متعدد المنصات</summary>
<summary xml:lang="ca">Client convergent de música de YouTube</summary>
@@ -54,7 +53,6 @@ SPDX-License-Identifier: CC0-1.0
<summary xml:lang="sv">Konvergent YouTube-musikprogram</summary>
<summary xml:lang="uk">Зручний музичний клієнт YouTube</summary>
<summary xml:lang="x-test">xxConvergent YouTube Music clientxx</summary>
<summary xml:lang="zh-CN">桌面和移动设备通用 YouTube Music 客户端</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-2.0+</project_license>
<developer_name>KDE Community</developer_name>
@@ -82,7 +80,6 @@ SPDX-License-Identifier: CC0-1.0
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxKDE Communityxx</developer_name>
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
<content_rating type="oars-1.0"/>
<url type="homepage">https://apps.kde.org/en/audiotube</url>
<releases>
@@ -137,6 +134,5 @@ SPDX-License-Identifier: CC0-1.0
<p xml:lang="sv">AudioTube kan söka på YouTube Music, lista album och artister, spela upp automatiskt genererade spellistor, albums och låter dig sammanställa egna spellistor.</p>
<p xml:lang="uk">AudioTube може виконувати пошук на YouTube Music, показувати списки альбомів та виконавців, відтворювати створені автоматично списки відтворення, альбоми та надає вам змогу створювати власні списки відворення.</p>
<p xml:lang="x-test">xxAudioTube can search YouTube Music, list albums and artists, play automatically generated playlists, albums and allows to put your own playlist together.xx</p>
<p xml:lang="zh-CN">AudioTube 可以搜索 YouTube Music 的内容,列出专辑和艺人,播放自动生成的播放列表,还可以自行制作播放列表。</p>
</description>
</component>
-2
View File
@@ -24,7 +24,6 @@ Name[sl]=AudioTube
Name[sv]=AudioTube
Name[uk]=AudioTube
Name[x-test]=xxAudioTubexx
Name[zh_CN]=AudioTube
Comment=YouTube Music client
Comment[ar]=عميل يوتيوب ميوزيك
Comment[ca]=Client de música de YouTube
@@ -50,7 +49,6 @@ Comment[sl]=Glasbeni odjemalec Youtube
Comment[sv]=YouTube-musikprogram
Comment[uk]=Музичний клієнт YouTube
Comment[x-test]=xxYouTube Music clientxx
Comment[zh_CN]=YouTube Music 客户端
Version=1.0
Exec=audiotube
Icon=org.kde.audiotube
-8
View File
@@ -1,8 +0,0 @@
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)
-24
View File
@@ -1,24 +0,0 @@
# 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()
-3
View File
@@ -1,3 +0,0 @@
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})
-17
View File
@@ -1,17 +0,0 @@
#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");
}
-193
View File
@@ -1,193 +0,0 @@
#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;
-66
View File
@@ -1,66 +0,0 @@
#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
View File
@@ -3,5 +3,5 @@
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
ytmusicapi==0.21.0
ytmusicapi==0.20.0
yt-dlp
-2
View File
@@ -26,7 +26,6 @@ add_executable(audiotube
playerutils.cpp
abstractytmusicmodel.cpp
multiiterableview.h
library.cpp
resources.qrc
)
@@ -41,7 +40,6 @@ target_link_libraries(audiotube
KF5::CoreAddons
KF5::Crash
ytm
qpersistentdata
)
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING)
+13 -14
View File
@@ -11,22 +11,21 @@ AlbumModel::AlbumModel(QObject *parent)
{
connect(this, &AlbumModel::browseIdChanged, this, [this] {
setLoading(true);
auto future = YTMusicThread::instance()->fetchAlbum(m_browseId);
connectFuture(future, this, [=, this](const album::Album &album) {
setLoading(false);
beginResetModel();
m_album = album;
endResetModel();
std::sort(m_album.thumbnails.begin(), m_album.thumbnails.end());
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
Q_EMIT playlistIdChanged();
});
YTMusicThread::instance()->fetchAlbum(m_browseId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [this](const album::Album &album) {
setLoading(false);
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=, this] {
beginResetModel();
m_album = album;
endResetModel();
std::sort(m_album.thumbnails.begin(), m_album.thumbnails.end());
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
Q_EMIT playlistIdChanged();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
setLoading(false);
});
}
+16 -16
View File
@@ -17,26 +17,26 @@ ArtistModel::ArtistModel(QObject *parent)
setLoading(true);
auto future = YTMusicThread::instance()->fetchArtist(m_channelId);
connectFuture(future, this, [=, this](const artist::Artist &artist) {
setLoading(false);
YTMusicThread::instance()->fetchArtist(m_channelId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [this](const artist::Artist &artist) {
setLoading(false);
beginResetModel();
m_artist = artist;
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
beginResetModel();
m_artist = artist;
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
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>();
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>();
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>();
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>();
// std::span can't know if the data pointer underneath it was changed, so re-create
m_view = MultiIterableView(albums, singles, songs, videos);
endResetModel();
// std::span can't know if the data pointer underneath it was changed, so re-create
m_view = MultiIterableView(albums, singles, songs, videos);
endResetModel();
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
});
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
setLoading(false);
+38 -35
View File
@@ -6,9 +6,6 @@
#include <QThread>
#include <QDebug>
#include <QJsonObject>
#include <QJsonArray>
#include <QFutureInterface>
#include <pybind11/embed.h>
@@ -16,13 +13,6 @@
namespace py = pybind11;
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <ranges>
#include <type_traits>
#include <memory>
template <typename R, typename T, typename OP>
std::optional<R> map_optional(const std::optional<T> &optional, OP op) {
if (optional.has_value()) {
@@ -59,82 +49,95 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
//
// search
//
QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QString &query)
void AsyncYTMusic::search(const QString &query)
{
return invokeAndCatchOnThread<std::vector<search::SearchResultItem>>([=, this]() {
return m_ytm->search(query.toStdString());
invokeAndCatchOnThread([=, this]() {
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
});
}
//
// fetchArtist
//
QFuture<artist::Artist> AsyncYTMusic::fetchArtist(const QString &channelId)
void AsyncYTMusic::fetchArtist(const QString &channelId)
{
return invokeAndCatchOnThread<artist::Artist>([=, this]() {
return m_ytm->get_artist(channelId.toStdString());
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
});
}
//
// fetchAlbum
//
QFuture<album::Album> AsyncYTMusic::fetchAlbum(const QString &browseId)
void AsyncYTMusic::fetchAlbum(const QString &browseId)
{
return invokeAndCatchOnThread<album::Album>([=, this]() {
return m_ytm->get_album(browseId.toStdString());
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
});
}
//
// fetchSong
//
QFuture<std::optional<song::Song>> AsyncYTMusic::fetchSong(const QString &videoId)
void AsyncYTMusic::fetchSong(const QString &videoId)
{
return invokeAndCatchOnThread<std::optional<song::Song>>([=, this]() {
return m_ytm->get_song(videoId.toStdString());
invokeAndCatchOnThread([=, this]() {
auto maybeSong = m_ytm->get_song(videoId.toStdString());
if (maybeSong.has_value()) {
Q_EMIT fetchSongFinished(*maybeSong);
}
});
}
//
// fetchPlaylist
//
QFuture<playlist::Playlist> AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
return invokeAndCatchOnThread<playlist::Playlist>([=, this]() {
return m_ytm->get_playlist(playlistId.toStdString());
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
});
}
//
// fetchArtistAlbum
//
QFuture<std::vector<artist::Artist::Album>> AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &params)
void AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &params)
{
return invokeAndCatchOnThread<std::vector<artist::Artist::Album>>([=, this]() {
return m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString());
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString()));
});
}
//
// extractVideoInfo
//
QFuture<video_info::VideoInfo> AsyncYTMusic::extractVideoInfo(const QString &videoId)
void AsyncYTMusic::extractVideoInfo(const QString &videoId)
{
return invokeAndCatchOnThread<video_info::VideoInfo>([=, this]() {
return m_ytm->extract_video_info(videoId.toStdString());
invokeAndCatchOnThread([=, this]() {
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
});
}
//
// fetchWatchPlaylist
//
QFuture<watch::Playlist> AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
void AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
{
return invokeAndCatchOnThread<watch::Playlist>([=, this]() {
return m_ytm->get_watch_playlist(
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
map_optional<std::string>(videoId, &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()));
}
});
}
+17 -36
View File
@@ -6,8 +6,6 @@
#include <QObject>
#include <QThread>
#include <QFuture>
#include <QFutureWatcher>
#include <vector>
@@ -47,17 +45,6 @@ private:
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
{
friend class YTMusicThread;
@@ -66,22 +53,30 @@ class AsyncYTMusic : public QObject
public:
// public functions need to be thread safe
QFuture<std::vector<search::SearchResultItem>> search(const QString &query);
void search(const QString &query);
Q_SIGNAL void searchFinished(std::vector<search::SearchResultItem>);
QFuture<artist::Artist> fetchArtist(const QString &channelId);
void fetchArtist(const QString &channelId);
Q_SIGNAL void fetchArtistFinished(artist::Artist);
QFuture<album::Album> fetchAlbum(const QString &browseId);
void fetchAlbum(const QString &browseId);
Q_SIGNAL void fetchAlbumFinished(album::Album);
QFuture<std::optional<song::Song> > fetchSong(const QString &videoId);
void fetchSong(const QString &videoId);
Q_SIGNAL void fetchSongFinished(song::Song);
QFuture<playlist::Playlist> fetchPlaylist(const QString &playlistId);
void fetchPlaylist(const QString &playlistId);
Q_SIGNAL void fetchPlaylistFinished(playlist::Playlist);
QFuture<std::vector<artist::Artist::Album>> fetchArtistAlbums(const QString &channelId, const QString &params);
void fetchArtistAlbums(const QString &channelId, const QString &params);
Q_SIGNAL void fetchArtistAlbumsFinished(std::vector<artist::Artist::Album>);
QFuture<video_info::VideoInfo> extractVideoInfo(const QString &videoId);
void extractVideoInfo(const QString &videoId);
Q_SIGNAL void extractVideoInfoFinished(video_info::VideoInfo);
QFuture<watch::Playlist> fetchWatchPlaylist(const std::optional<QString> &videoId = std::nullopt ,
void fetchWatchPlaylist(const std::optional<QString> &videoId = std::nullopt ,
const std::optional<QString> &playlistId = std::nullopt);
Q_SIGNAL void fetchWatchPlaylistFinished(watch::Playlist);
Q_SIGNAL void errorOccurred(const QString &error);
@@ -90,21 +85,7 @@ protected:
private:
/// Invokes the given function on the thread of the YTMusic object, and handles exceptions that occur while invoking it.
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();
}
void invokeAndCatchOnThread(const std::function<void()> &fun);
// Python interpreter will be initialized from the thread calling the methods
Lazy<YTMusic> m_ytm;
-54
View File
@@ -1,54 +0,0 @@
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
}
}
}
}
}
}
}
+3 -6
View File
@@ -38,18 +38,15 @@ Item {
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
visible: footerItem.maximized
ColumnLayout {
Item {
Layout.fillWidth: info.thumbnail
Layout.fillHeight: info.thumbnail
Image {
anchors.fill: parent
source: info.thumbnail
Layout.preferredWidth: footerLayout.width * 0.5
fillMode: Image.PreserveAspectFit
}
Controls.ToolButton{
text: "Fav"
onClicked: Library.addFavourite(info.videoId)
}
}
Item {
+1 -40
View File
@@ -36,49 +36,10 @@ Kirigami.ApplicationWindow {
selectByMouse: true
Layout.fillWidth: true
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: {
pageStack.clear()
if (text) {
Library.addSearch(text)
}
pageStack.push("qrc:/SearchPage.qml", {
"searchQuery": text})
popup.close()
}
}
Item {
@@ -95,7 +56,7 @@ Kirigami.ApplicationWindow {
id: contextDrawer
}
pageStack.initialPage: "qrc:/LibraryPage.qml"
pageStack.initialPage: "qrc:/SearchPage.qml"
function play(videoId) {
UserPlaylistModel.initialVideoId = videoId
-111
View File
@@ -1,111 +0,0 @@
#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));
});
});
}
-70
View File
@@ -1,70 +0,0 @@
#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;
};
-5
View File
@@ -19,7 +19,6 @@
#include "errorhandler.h"
#include "playlistmodel.h"
#include "playerutils.h"
#include "library.h"
constexpr auto URI = "org.kde.ytmusic";
@@ -52,10 +51,6 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
qmlRegisterSingletonType<PlayerUtils>(URI, 1, 0, "PlayerUtils", [](QQmlEngine *, QJSEngine *) {
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.load(QUrl(QStringLiteral("qrc:///main.qml")));
+10 -10
View File
@@ -15,17 +15,17 @@ PlaylistModel::PlaylistModel(QObject *parent)
{
connect(this, &PlaylistModel::playlistIdChanged, this, [=, this] {
setLoading(true);
auto future = YTMusicThread::instance()->fetchPlaylist(m_playlistId);
connectFuture(future, this, [=, this](const playlist::Playlist &playlist) {
setLoading(false);
beginResetModel();
m_playlist = playlist;
std::sort(m_playlist.thumbnails.begin(), m_playlist.thumbnails.end());
endResetModel();
YTMusicThread::instance()->fetchPlaylist(m_playlistId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [this](const playlist::Playlist &playlist) {
setLoading(false);
beginResetModel();
m_playlist = playlist;
std::sort(m_playlist.thumbnails.begin(), m_playlist.thumbnails.end());
endResetModel();
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
});
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
});
}
+6 -1
View File
@@ -1,3 +1,9 @@
<!--
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>
<qresource prefix="/">
<file alias="main.qml">contents/ui/main.qml</file>
@@ -7,6 +13,5 @@
<file alias="SearchPage.qml">contents/ui/SearchPage.qml</file>
<file alias="PlaylistPage.qml">contents/ui/PlaylistPage.qml</file>
<file alias="ConfirmationMessage.qml">contents/ui/ConfirmationMessage.qml</file>
<file alias="LibraryPage.qml">contents/ui/LibraryPage.qml</file>
</qresource>
</RCC>
+11 -7
View File
@@ -16,13 +16,17 @@ SearchModel::SearchModel(QObject *parent)
}
setLoading(true);
auto future = YTMusicThread::instance()->search(m_searchQuery);
connectFuture(future, this, [=, this](const std::vector<search::SearchResultItem> &results) {
beginResetModel();
setLoading(false);
m_searchResults = results;
endResetModel();
});
YTMusicThread::instance()->search(m_searchQuery);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [this](const std::vector<search::SearchResultItem> &results) {
beginResetModel();
setLoading(false);
m_searchResults = results;
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] {
setLoading(false);
+17 -20
View File
@@ -9,14 +9,29 @@
#include <random>
#include <QRandomGenerator>
#include <QFutureWatcher>
#include "playlistutils.h"
UserPlaylistModel::UserPlaylistModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
auto handleResult = [=, this](const watch::Playlist &playlist) {
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [this] {
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);
beginResetModel();
@@ -33,24 +48,6 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
} else {
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] {
setLoading(false);
+10 -17
View File
@@ -4,10 +4,7 @@
#include "videoinfoextractor.h"
#include <QFutureWatcher>
#include "asyncytmusic.h"
#include "library.h"
#include <asyncytmusic.h>
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
: QObject(parent)
@@ -23,17 +20,16 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
}
setLoading(true);
YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
});
auto future = YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
connectFuture(future, this, [this](const video_info::VideoInfo &videoInfo) {
m_videoInfo = videoInfo;
setLoading(false);
Library::instance().addVideoTitle(m_videoId, QString::fromStdString(m_videoInfo.title));
Q_EMIT audioUrlChanged();
Q_EMIT titleChanged();
Q_EMIT songChanged();
Q_EMIT thumbnailChanged();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [this](const video_info::VideoInfo &info) {
m_videoInfo = info;
setLoading(false);
Q_EMIT audioUrlChanged();
Q_EMIT titleChanged();
Q_EMIT songChanged();
Q_EMIT thumbnailChanged();
});
}
@@ -70,9 +66,6 @@ QString VideoInfoExtractor::videoId() const
void VideoInfoExtractor::setVideoId(const QString &videoId)
{
if (m_videoId == videoId || videoId.isEmpty()) {
return;
}
m_videoId = videoId;
Q_EMIT videoIdChanged();
}