Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae78302af | ||
|
|
7ec01c5854 | ||
|
|
2ae7ce866a | ||
|
|
424c67a268 | ||
|
|
64d7a911fa | ||
|
|
961dca328d | ||
|
|
433d95969c | ||
|
|
15f439c38e | ||
|
|
ca4d5d8b77 | ||
|
|
06338900dc | ||
|
|
8ec674fbef | ||
|
|
58eefc29eb | ||
|
|
9312331755 | ||
|
|
1d25f46f52 | ||
|
|
a775cd58be | ||
|
|
ae5ed3b10b | ||
|
|
8aab91568e | ||
|
|
6647346314 | ||
|
|
bfb41310ad | ||
|
|
c3371adda4 | ||
|
|
9bcf017c8d | ||
|
|
2c0b13ff9f | ||
|
|
abfd901a82 | ||
|
|
135b8859d6 | ||
|
|
f8637b686c | ||
|
|
53f466c976 |
No files matched your search
@@ -6,3 +6,4 @@
|
||||
*.kdev4
|
||||
CMakeLists.txt.user
|
||||
flatpak/flatpak-builder-tools/
|
||||
build/
|
||||
+21
@@ -2,3 +2,24 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: AudioTube
|
||||
Upstream-Contact: Jonah Brüchert <[email protected]>
|
||||
Source: https://invent.kde.org/plasma-mobile/audiotube
|
||||
|
||||
# Sample paragraph, commented out:
|
||||
#
|
||||
# Files: ...
|
||||
# Copyright: $YEAR $NAME <$CONTACT>
|
||||
# License: ...
|
||||
# Comment: ...
|
||||
|
||||
Files: README.md
|
||||
Copyright: 2021 Jonah Brüchert <[email protected]>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: org.kde.audiotube.desktop
|
||||
org.kde.audiotube.json
|
||||
Copyright: 2021 Jonah Brüchert <[email protected]>
|
||||
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
Files: logo.png
|
||||
org.kde.audiotube.svg
|
||||
Copyright: 2021 Mathis Brüchert <[email protected]>
|
||||
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
+3
-2
@@ -8,11 +8,11 @@ project(audiotube)
|
||||
|
||||
include(FeatureSummary)
|
||||
|
||||
set(PROJECT_VERSION "21.12")
|
||||
set(PROJECT_VERSION "22.02")
|
||||
set(QT5_MIN_VERSION 5.15)
|
||||
set(KF5_MIN_VERSION 5.81)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
@@ -40,6 +40,7 @@ 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})
|
||||
|
||||
@@ -6,7 +6,7 @@ AudioTube is a client for YouTube Music.
|
||||
|
||||
It depends on Qt, Kirigami, KI18n, KCoreAddons and KCrash in addition to a few python packages:
|
||||
* ytmusicapi
|
||||
* youtube_dl
|
||||
* yt_dlp
|
||||
|
||||
YouTube uses webp images for the cover art, so you will need to install qtimageformats.
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
@@ -7,13 +7,13 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/5e/2a/3cbe2e29926784d8718eb2f92357d7e22caf393f24e19287746076ffc9f2/yt_dlp-2021.11.10.1-py2.py3-none-any.whl",
|
||||
"sha256": "9c3d85fdeeac3d61cfc85fd72d55fe6b63fcf1d19d05e2841cf2e544922ed157"
|
||||
"url": "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl",
|
||||
"sha256": "d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/69/77/591bbc51a5ed6a906a7813e60a9627f988f9546513fcf9d250eb31ec8689/websockets-10.1.tar.gz",
|
||||
"sha256": "181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"
|
||||
"url": "https://files.pythonhosted.org/packages/f8/a3/622d9acbfb9a71144b5d7609906bc648c62e3ca5fdbb1c8cca222949d82c/websockets-10.3.tar.gz",
|
||||
"sha256": "fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
@@ -22,8 +22,18 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/47/14/dd9ad29cd29ea4cc521286f2cb401ca7ac6fd5db0791c5e9bacaf2c9ac78/pycryptodomex-3.11.0.tar.gz",
|
||||
"sha256": "0398366656bb55ebdb1d1d493a7175fc48ade449283086db254ac44c7d318d6d"
|
||||
"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,18 +7,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/7b/77/f341b20e7307f18ede4625867cbb8e18c20199117e0c23f7c6b53fe6597d/ytmusicapi-0.19.4-py3-none-any.whl",
|
||||
"sha256": "d0e980b35cf290eae409dcdc6ee31c5c358b28baebf1052e9fc9f446fc7901b7"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/92/96/144f70b972a9c0eabbd4391ef93ccd49d0f2747f4f6a2a2738e99e5adc65/requests-2.26.0-py2.py3-none-any.whl",
|
||||
"sha256": "6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/c8/27/141554fc0f42c05dd318fbb0be0e3e018da686544a3ff452762e49ccac58/charset_normalizer-2.0.8-py3-none-any.whl",
|
||||
"sha256": "83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405"
|
||||
"url": "https://files.pythonhosted.org/packages/cb/58/834a6445479c72544d9285badce0a58650520c8ba5adfae7b98e61d133bb/ytmusicapi-0.21.0-py3-none-any.whl",
|
||||
"sha256": "29d8d44e1911458041882a453b05c37021ff850757c648f93a45d1775dddbae2"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
@@ -27,13 +17,23 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
|
||||
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
|
||||
"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/af/f4/524415c0744552cce7d8bf3669af78e8a069514405ea4fcbd0cc44733744/urllib3-1.26.7-py2.py3-none-any.whl",
|
||||
"sha256": "c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||
"url": "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl",
|
||||
"sha256": "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl",
|
||||
"sha256": "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
|
||||
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2021 Mathis Brüchert <[email protected]>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
@@ -7,6 +7,7 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<component type="desktop-application">
|
||||
<id>org.kde.audiotube</id>
|
||||
<name>AudioTube</name>
|
||||
<name xml:lang="ar">تيوب الصوتي</name>
|
||||
<name xml:lang="ca">AudioTube</name>
|
||||
<name xml:lang="ca-valencia">AudioTube</name>
|
||||
<name xml:lang="cs">AudioTube</name>
|
||||
@@ -24,12 +25,15 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<name xml:lang="pl">AudioTube</name>
|
||||
<name xml:lang="pt">AudioTube</name>
|
||||
<name xml:lang="pt-BR">AudioTube</name>
|
||||
<name xml:lang="ro">AudioTube</name>
|
||||
<name xml:lang="sk">AudioTube</name>
|
||||
<name xml:lang="sl">AudioTube</name>
|
||||
<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>
|
||||
<summary xml:lang="ca-valencia">Client convergent de música de YouTube</summary>
|
||||
<summary xml:lang="de">Konvergentes YouTube-Musikprogramm</summary>
|
||||
@@ -45,13 +49,16 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<summary xml:lang="pl">Adaptacyjny program do YouTube Music</summary>
|
||||
<summary xml:lang="pt">Cliente de música do YouTube convergente</summary>
|
||||
<summary xml:lang="pt-BR">Um cliente convergente para o YouTube Music</summary>
|
||||
<summary xml:lang="ro">Client convergent de muzică pentru YouTube</summary>
|
||||
<summary xml:lang="sl">Konvergentni odjemalec glasbe YouTube</summary>
|
||||
<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>
|
||||
<developer_name xml:lang="ar">مجتمع كدي</developer_name>
|
||||
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
|
||||
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
|
||||
<developer_name xml:lang="cs">Komunita KDE</developer_name>
|
||||
@@ -69,14 +76,24 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
|
||||
<developer_name xml:lang="pt">Comunidade do KDE</developer_name>
|
||||
<developer_name xml:lang="pt-BR">A Comunidade KDE</developer_name>
|
||||
<developer_name xml:lang="ro">Comunitatea KDE</developer_name>
|
||||
<developer_name xml:lang="sk">KDE komunita</developer_name>
|
||||
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
|
||||
<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>
|
||||
<release version="22.02" date="2022-02-09">
|
||||
<url>https://www.plasma-mobile.org/2022/02/09/plasma-mobile-gear-22-02/</url>
|
||||
<description>
|
||||
<ul>
|
||||
<li>Update the version of ytmusicapi</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="21.12" date="2021-12-07">
|
||||
<url>https://www.plasma-mobile.org/2021/12/07/plasma-mobile-gear-21-12/</url>
|
||||
</release>
|
||||
@@ -100,6 +117,7 @@ SPDX-License-Identifier: CC0-1.0
|
||||
</recommends>
|
||||
<description>
|
||||
<p>AudioTube can search YouTube Music, list albums and artists, play automatically generated playlists, albums and allows to put your own playlist together.</p>
|
||||
<p xml:lang="ar">يمكن لتيوب الصوتي البحث في موسيقى يوتيوب، وعرض الألبومات والفنانين، وتشغيل القوائم والألبومات المولدة تلقائيًا، ويسمح لك بوضع قائمة التشغيل الخاصة بك معًا.</p>
|
||||
<p xml:lang="ca">L'AudioTube pot cercar música a YouTube, llistar àlbums i artistes, reproduir llistes de reproducció generades automàticament, àlbums i permet crear la vostra pròpia llista de reproducció.</p>
|
||||
<p xml:lang="ca-valencia">L'AudioTube pot cercar música a YouTube, llistar àlbums i artistes, reproduir llistes de reproducció generades automàticament, àlbums i permet crear la vostra pròpia llista de reproducció.</p>
|
||||
<p xml:lang="de">AudioTube kann Musik auf YouTube durchsuchen, Alben und Interpreten auflisten, automatisch generierte Wiedergabelisten und Alben abspielen und erlaubt es, eine eigene Wiedergabeliste zusammenzustellen.</p>
|
||||
@@ -119,5 +137,6 @@ 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>
|
||||
@@ -1,5 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=AudioTube
|
||||
Name[ar]=تيوب الصوتي
|
||||
Name[ca]=AudioTube
|
||||
Name[ca@valencia]=AudioTube
|
||||
Name[cs]=AudioTube
|
||||
@@ -23,7 +24,9 @@ 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
|
||||
Comment[ca@valencia]=Client de música de YouTube
|
||||
Comment[cs]=Hudební klient YouTube
|
||||
@@ -47,6 +50,7 @@ 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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
@@ -28,7 +28,7 @@
|
||||
"config-opts": ["-DPYBIND11_FINDPYTHON=ON", "-DPYBIND11_TEST=OFF"],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"builddir": true,
|
||||
"sources": [ { "type": "git", "url": "https://github.com/pybind/pybind11", "tag": "v2.6.2" } ]
|
||||
"sources": [ { "type": "git", "url": "https://github.com/pybind/pybind11", "tag": "v2.9.1" } ]
|
||||
},
|
||||
{
|
||||
"name": "org.kde.audiotube",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2021 Mathis Brüchert <[email protected]>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
@@ -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;
|
||||
};
|
||||
+2
-2
@@ -3,5 +3,5 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
ytmusicapi==0.19.4
|
||||
ytdlp
|
||||
ytmusicapi==0.21.0
|
||||
yt-dlp
|
||||
@@ -26,6 +26,7 @@ add_executable(audiotube
|
||||
playerutils.cpp
|
||||
abstractytmusicmodel.cpp
|
||||
multiiterableview.h
|
||||
library.cpp
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
@@ -40,6 +41,7 @@ target_link_libraries(audiotube
|
||||
KF5::CoreAddons
|
||||
KF5::Crash
|
||||
ytm
|
||||
qpersistentdata
|
||||
)
|
||||
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING)
|
||||
|
||||
|
||||
+14
-13
@@ -9,23 +9,24 @@
|
||||
AlbumModel::AlbumModel(QObject *parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &AlbumModel::browseIdChanged, this, [=] {
|
||||
connect(this, &AlbumModel::browseIdChanged, this, [this] {
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->fetchAlbum(m_browseId);
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [=](const album::Album &album) {
|
||||
setLoading(false);
|
||||
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());
|
||||
beginResetModel();
|
||||
m_album = album;
|
||||
endResetModel();
|
||||
std::sort(m_album.thumbnails.begin(), m_album.thumbnails.end());
|
||||
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT thumbnailUrlChanged();
|
||||
Q_EMIT playlistIdChanged();
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT thumbnailUrlChanged();
|
||||
Q_EMIT playlistIdChanged();
|
||||
});
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
|
||||
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=, this] {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
+41
-49
@@ -8,54 +8,48 @@
|
||||
|
||||
ArtistModel::ArtistModel(QObject *parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
, m_view(albums, singles, songs, videos)
|
||||
{
|
||||
connect(this, &ArtistModel::channelIdChanged, this, [=] {
|
||||
connect(this, &ArtistModel::channelIdChanged, this, [this] {
|
||||
if (m_channelId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
YTMusicThread::instance()->fetchArtist(m_channelId);
|
||||
auto future = YTMusicThread::instance()->fetchArtist(m_channelId);
|
||||
connectFuture(future, this, [=, this](const artist::Artist &artist) {
|
||||
setLoading(false);
|
||||
|
||||
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>();
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [=](const artist::Artist &artist) {
|
||||
setLoading(false);
|
||||
|
||||
beginResetModel();
|
||||
m_artist = artist;
|
||||
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
|
||||
|
||||
auto albums = m_artist.albums ? m_artist.albums->results : std::vector<artist::Artist::Album>();
|
||||
auto singles = m_artist.singles ? m_artist.singles->results : std::vector<artist::Artist::Single>();
|
||||
auto songs = m_artist.songs ? m_artist.songs->results : std::vector<artist::Artist::Song>();
|
||||
auto videos = m_artist.videos ? m_artist.videos->results : std::vector<artist::Artist::Video>();
|
||||
|
||||
m_view = std::optional(MultiIterableView(std::move(albums),
|
||||
std::move(singles),
|
||||
std::move(songs),
|
||||
std::move(videos)));
|
||||
endResetModel();
|
||||
|
||||
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT thumbnailUrlChanged();
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
int ArtistModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_view ? int(m_view->size()) : 0;
|
||||
return parent.isValid() ? 0 : m_view.size();
|
||||
}
|
||||
|
||||
QVariant ArtistModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!m_view) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Title:
|
||||
return QString::fromStdString(std::visit([&](auto&& item) {
|
||||
@@ -71,7 +65,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
}, m_view.value()[index.row()]));
|
||||
}, m_view[index.row()]));
|
||||
case Type:
|
||||
return std::visit([&](auto&& item) {
|
||||
using T = std::decay_t<decltype(item)>;
|
||||
@@ -86,7 +80,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
}, m_view.value()[index.row()]);
|
||||
}, m_view[index.row()]);
|
||||
case Artists:
|
||||
return QVariant::fromValue(std::vector<meta::Artist> {
|
||||
{
|
||||
@@ -108,7 +102,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
}, m_view.value()[index.row()]);
|
||||
}, m_view[index.row()]);
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
@@ -153,20 +147,18 @@ QUrl ArtistModel::thumbnailUrl() const
|
||||
|
||||
void ArtistModel::triggerItem(int row)
|
||||
{
|
||||
if (m_view) {
|
||||
std::visit([&](auto&& item) {
|
||||
using T = std::decay_t<decltype(item)>;
|
||||
if constexpr(std::is_same_v<T, artist::Artist::Album>) {
|
||||
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Single>) {
|
||||
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Song>) {
|
||||
Q_EMIT openSong(QString::fromStdString(item.video_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Video>) {
|
||||
Q_EMIT openVideo(QString::fromStdString(item.video_id), QString::fromStdString(item.title));
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}, m_view.value()[row]);
|
||||
}
|
||||
std::visit([&](auto&& item) {
|
||||
using T = std::decay_t<decltype(item)>;
|
||||
if constexpr(std::is_same_v<T, artist::Artist::Album>) {
|
||||
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Single>) {
|
||||
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Song>) {
|
||||
Q_EMIT openSong(QString::fromStdString(item.video_id));
|
||||
} else if constexpr(std::is_same_v<T, artist::Artist::Video>) {
|
||||
Q_EMIT openVideo(QString::fromStdString(item.video_id), QString::fromStdString(item.title));
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}, m_view[row]);
|
||||
}
|
||||
+8
-2
@@ -66,7 +66,13 @@ private:
|
||||
QString m_channelId;
|
||||
|
||||
artist::Artist m_artist;
|
||||
std::optional<MultiIterableView<
|
||||
|
||||
std::vector<artist::Artist::Album> albums;
|
||||
std::vector<artist::Artist::Single> singles;
|
||||
std::vector<artist::Artist::Song> songs;
|
||||
std::vector<artist::Artist::Video> videos;
|
||||
|
||||
MultiIterableView<
|
||||
artist::Artist::Album, artist::Artist::Single, artist::Artist::Song, artist::Artist::Video
|
||||
>> m_view;
|
||||
> m_view;
|
||||
};
|
||||
+35
-38
@@ -6,6 +6,9 @@
|
||||
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFutureInterface>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
@@ -13,6 +16,13 @@
|
||||
|
||||
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()) {
|
||||
@@ -49,95 +59,82 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
|
||||
//
|
||||
// search
|
||||
//
|
||||
void AsyncYTMusic::search(const QString &query)
|
||||
QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QString &query)
|
||||
{
|
||||
invokeAndCatchOnThread([=]() {
|
||||
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
|
||||
return invokeAndCatchOnThread<std::vector<search::SearchResultItem>>([=, this]() {
|
||||
return m_ytm->search(query.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// fetchArtist
|
||||
//
|
||||
void AsyncYTMusic::fetchArtist(const QString &channelId)
|
||||
QFuture<artist::Artist> AsyncYTMusic::fetchArtist(const QString &channelId)
|
||||
{
|
||||
invokeAndCatchOnThread([=]() {
|
||||
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
|
||||
return invokeAndCatchOnThread<artist::Artist>([=, this]() {
|
||||
return m_ytm->get_artist(channelId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// fetchAlbum
|
||||
//
|
||||
void AsyncYTMusic::fetchAlbum(const QString &browseId)
|
||||
QFuture<album::Album> AsyncYTMusic::fetchAlbum(const QString &browseId)
|
||||
{
|
||||
invokeAndCatchOnThread([=]() {
|
||||
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
|
||||
return invokeAndCatchOnThread<album::Album>([=, this]() {
|
||||
return m_ytm->get_album(browseId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// fetchSong
|
||||
//
|
||||
void AsyncYTMusic::fetchSong(const QString &videoId)
|
||||
QFuture<std::optional<song::Song>> AsyncYTMusic::fetchSong(const QString &videoId)
|
||||
{
|
||||
invokeAndCatchOnThread([=]() {
|
||||
auto maybeSong = m_ytm->get_song(videoId.toStdString());
|
||||
if (maybeSong.has_value()) {
|
||||
Q_EMIT fetchSongFinished(*maybeSong);
|
||||
}
|
||||
return invokeAndCatchOnThread<std::optional<song::Song>>([=, this]() {
|
||||
return m_ytm->get_song(videoId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// fetchPlaylist
|
||||
//
|
||||
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
|
||||
invokeAndCatchOnThread([=]() {
|
||||
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
|
||||
QFuture<playlist::Playlist> AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
|
||||
return invokeAndCatchOnThread<playlist::Playlist>([=, this]() {
|
||||
return m_ytm->get_playlist(playlistId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// 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([=]() {
|
||||
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString()));
|
||||
return invokeAndCatchOnThread<std::vector<artist::Artist::Album>>([=, this]() {
|
||||
return m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// extractVideoInfo
|
||||
//
|
||||
void AsyncYTMusic::extractVideoInfo(const QString &videoId)
|
||||
QFuture<video_info::VideoInfo> AsyncYTMusic::extractVideoInfo(const QString &videoId)
|
||||
{
|
||||
invokeAndCatchOnThread([=]() {
|
||||
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
|
||||
return invokeAndCatchOnThread<video_info::VideoInfo>([=, this]() {
|
||||
return m_ytm->extract_video_info(videoId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// 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([=]() {
|
||||
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
|
||||
return invokeAndCatchOnThread<watch::Playlist>([=, this]() {
|
||||
return 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, [=]() {
|
||||
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 <QThread>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -45,6 +47,17 @@ 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;
|
||||
@@ -53,30 +66,22 @@ class AsyncYTMusic : public QObject
|
||||
|
||||
public:
|
||||
// public functions need to be thread safe
|
||||
void search(const QString &query);
|
||||
Q_SIGNAL void searchFinished(std::vector<search::SearchResultItem>);
|
||||
QFuture<std::vector<search::SearchResultItem>> search(const QString &query);
|
||||
|
||||
void fetchArtist(const QString &channelId);
|
||||
Q_SIGNAL void fetchArtistFinished(artist::Artist);
|
||||
QFuture<artist::Artist> fetchArtist(const QString &channelId);
|
||||
|
||||
void fetchAlbum(const QString &browseId);
|
||||
Q_SIGNAL void fetchAlbumFinished(album::Album);
|
||||
QFuture<album::Album> fetchAlbum(const QString &browseId);
|
||||
|
||||
void fetchSong(const QString &videoId);
|
||||
Q_SIGNAL void fetchSongFinished(song::Song);
|
||||
QFuture<std::optional<song::Song> > fetchSong(const QString &videoId);
|
||||
|
||||
void fetchPlaylist(const QString &playlistId);
|
||||
Q_SIGNAL void fetchPlaylistFinished(playlist::Playlist);
|
||||
QFuture<playlist::Playlist> fetchPlaylist(const QString &playlistId);
|
||||
|
||||
void fetchArtistAlbums(const QString &channelId, const QString ¶ms);
|
||||
Q_SIGNAL void fetchArtistAlbumsFinished(std::vector<artist::Artist::Album>);
|
||||
QFuture<std::vector<artist::Artist::Album>> fetchArtistAlbums(const QString &channelId, const QString ¶ms);
|
||||
|
||||
void extractVideoInfo(const QString &videoId);
|
||||
Q_SIGNAL void extractVideoInfoFinished(video_info::VideoInfo);
|
||||
QFuture<video_info::VideoInfo> extractVideoInfo(const QString &videoId);
|
||||
|
||||
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);
|
||||
Q_SIGNAL void fetchWatchPlaylistFinished(watch::Playlist);
|
||||
|
||||
Q_SIGNAL void errorOccurred(const QString &error);
|
||||
|
||||
@@ -85,7 +90,21 @@ protected:
|
||||
|
||||
private:
|
||||
/// 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
|
||||
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
|
||||
visible: footerItem.maximized
|
||||
|
||||
Item {
|
||||
ColumnLayout {
|
||||
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 {
|
||||
|
||||
@@ -36,10 +36,49 @@ 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 {
|
||||
@@ -56,7 +95,7 @@ Kirigami.ApplicationWindow {
|
||||
id: contextDrawer
|
||||
}
|
||||
|
||||
pageStack.initialPage: "qrc:/SearchPage.qml"
|
||||
pageStack.initialPage: "qrc:/LibraryPage.qml"
|
||||
|
||||
function play(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 "playlistmodel.h"
|
||||
#include "playerutils.h"
|
||||
#include "library.h"
|
||||
|
||||
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 *) {
|
||||
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")));
|
||||
|
||||
+49
-52
@@ -6,71 +6,68 @@
|
||||
|
||||
#include <variant>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
#include <QtGlobal>
|
||||
|
||||
// MultiIterableView is usually used on a small number of vectors.
|
||||
// The number is already known, so we can convert this into an if else if else etc tree
|
||||
// instead of recursively calling the function
|
||||
#ifdef __GNUC__
|
||||
#define REMOVE_RECURSION __attribute__((always_inline))
|
||||
#else
|
||||
#define REMOVE_RECURSION
|
||||
#endif
|
||||
template <typename Tuple, typename Func, std::size_t i>
|
||||
inline constexpr void iterate_impl(Tuple &tup, Func fun)
|
||||
{
|
||||
if constexpr(i >= std::tuple_size_v<std::decay_t<decltype(tup)>>) {
|
||||
return;
|
||||
} else {
|
||||
fun(std::get<i>(tup));
|
||||
return iterate_impl<Tuple, Func, i + 1>(tup, fun);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Func>
|
||||
inline constexpr void iterate_tuple(Tuple &tup, Func fun)
|
||||
{
|
||||
iterate_impl<Tuple, Func, 0>(tup, fun);
|
||||
}
|
||||
|
||||
/// MultiIterableView is a view over multiple contigous memory arrays, like std::vector or std::array.
|
||||
/// The elements of the arrays do not need to be of the same type.
|
||||
template <typename... Arguments>
|
||||
class MultiIterableView {
|
||||
private:
|
||||
template<typename T, size_t i, size_t len>
|
||||
REMOVE_RECURSION inline size_t count(const T &tuple, int initial_size = 0) const {
|
||||
if constexpr(i < len) {
|
||||
return count<T, i + 1, len>(tuple, initial_size + std::get<i>(tuple).size());
|
||||
} else {
|
||||
return initial_size;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, size_t tuple_i, size_t tuple_len>
|
||||
REMOVE_RECURSION inline std::variant<Arguments...> get_item(const T &tuple, size_t i) const {
|
||||
if constexpr(tuple_i < tuple_len) {
|
||||
if (i < std::get<tuple_i>(tuple).size()) {
|
||||
return std::get<tuple_i>(tuple)[i];
|
||||
} else {
|
||||
return get_item<T, tuple_i + 1, tuple_len>(tuple, i - std::get<tuple_i>(tuple).size());
|
||||
}
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, size_t i, size_t len>
|
||||
REMOVE_RECURSION inline bool everything_empty(const T &tuple, bool initial_state = true) const {
|
||||
if constexpr(i < len) {
|
||||
return everything_empty<T, i + 1, len>(tuple, initial_state && std::get<i>(tuple).empty());
|
||||
} else {
|
||||
return initial_state;
|
||||
}
|
||||
}
|
||||
public:
|
||||
explicit MultiIterableView(std::vector<Arguments> &&...lists)
|
||||
: m_vectors(std::make_tuple(lists...))
|
||||
explicit MultiIterableView(std::span<Arguments>... lists)
|
||||
: m_vectors(std::forward_as_tuple(lists...))
|
||||
{
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
|
||||
return count<decltype(m_vectors), 0, nvecs>(m_vectors);
|
||||
explicit MultiIterableView(std::vector<Arguments> &...lists)
|
||||
: MultiIterableView(std::span{lists}...)
|
||||
{
|
||||
}
|
||||
|
||||
std::variant<Arguments...> operator[](size_t i) const {
|
||||
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
|
||||
return get_item<decltype(m_vectors), 0, nvecs>(m_vectors, i);
|
||||
[[nodiscard]] constexpr size_t size() const {
|
||||
size_t s = 0;
|
||||
iterate_tuple(m_vectors, [&](auto &vec) {
|
||||
s += vec.size();
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
|
||||
return everything_empty<decltype(m_vectors), 0, nvecs>(m_vectors);
|
||||
[[nodiscard]] constexpr std::variant<Arguments...> operator[](size_t i) const {
|
||||
size_t s = 0;
|
||||
std::variant<Arguments...> out;
|
||||
iterate_tuple(m_vectors, [&](auto &vec) {
|
||||
if (i >= s && i < s + vec.size()) {
|
||||
out = vec[i - s];
|
||||
}
|
||||
s += vec.size();
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool empty() const {
|
||||
bool empty = true;
|
||||
iterate_tuple(m_vectors, [&](auto &elem) {
|
||||
empty &= elem.empty();
|
||||
});
|
||||
return empty;
|
||||
}
|
||||
private:
|
||||
std::tuple<std::vector<Arguments>...> m_vectors;
|
||||
std::tuple<std::span<Arguments>...> m_vectors;
|
||||
};
|
||||
+11
-11
@@ -13,19 +13,19 @@
|
||||
PlaylistModel::PlaylistModel(QObject *parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &PlaylistModel::playlistIdChanged, this, [=] {
|
||||
connect(this, &PlaylistModel::playlistIdChanged, this, [=, this] {
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->fetchPlaylist(m_playlistId);
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [=](const playlist::Playlist &playlist) {
|
||||
setLoading(false);
|
||||
beginResetModel();
|
||||
m_playlist = playlist;
|
||||
std::sort(m_playlist.thumbnails.begin(), m_playlist.thumbnails.end());
|
||||
endResetModel();
|
||||
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();
|
||||
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT thumbnailUrlChanged();
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT thumbnailUrlChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,12 @@ namespace PlaylistUtils {
|
||||
QString artistsToString(const std::vector<meta::Artist> &artists)
|
||||
{
|
||||
if (!artists.empty()) {
|
||||
return std::accumulate(artists.begin() + 1, artists.end(),
|
||||
QString::fromStdString(artists.front().name),
|
||||
[](QString &string, const meta::Artist &artist) {
|
||||
return string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
|
||||
QString string = QString::fromStdString(artists.front().name);
|
||||
std::for_each(artists.begin() + 1, artists.end(),
|
||||
[&string](const meta::Artist &artist) {
|
||||
string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
+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>
|
||||
<qresource prefix="/">
|
||||
<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="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>
|
||||
+30
-9
@@ -7,7 +7,7 @@
|
||||
SearchModel::SearchModel(QObject *parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &SearchModel::searchQueryChanged, this, [=] {
|
||||
connect(this, &SearchModel::searchQueryChanged, this, [this] {
|
||||
if (m_searchQuery.isEmpty()) {
|
||||
beginResetModel();
|
||||
m_searchResults.clear();
|
||||
@@ -16,15 +16,15 @@ SearchModel::SearchModel(QObject *parent)
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->search(m_searchQuery);
|
||||
auto future = YTMusicThread::instance()->search(m_searchQuery);
|
||||
connectFuture(future, this, [=, this](const std::vector<search::SearchResultItem> &results) {
|
||||
beginResetModel();
|
||||
setLoading(false);
|
||||
m_searchResults = results;
|
||||
endResetModel();
|
||||
});
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [=](const std::vector<search::SearchResultItem> &results) {
|
||||
beginResetModel();
|
||||
setLoading(false);
|
||||
m_searchResults = results;
|
||||
endResetModel();
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
@@ -165,3 +165,24 @@ void SearchModel::triggerItem(int row)
|
||||
}
|
||||
}, m_searchResults.at(row));
|
||||
}
|
||||
|
||||
|
||||
int SearchModel::itemType(search::SearchResultItem const &item)
|
||||
{
|
||||
return std::visit([&](auto &&arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr(std::is_same_v<T, search::Album>) {
|
||||
return SearchModel::Type::Album;
|
||||
} else if constexpr(std::is_same_v<T, search::Artist>) {
|
||||
return SearchModel::Type::Artist;
|
||||
} else if constexpr(std::is_same_v<T, search::Playlist>) {
|
||||
return SearchModel::Type::Playlist;
|
||||
} else if constexpr(std::is_same_v<T, search::Song>) {
|
||||
return SearchModel::Type::Song;
|
||||
} else if constexpr(std::is_same_v<T, search::Video>) {
|
||||
return SearchModel::Type::Video;
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}, item);
|
||||
}
|
||||
@@ -55,4 +55,5 @@ public:
|
||||
private:
|
||||
QString m_searchQuery;
|
||||
std::vector<search::SearchResultItem> m_searchResults;
|
||||
static int itemType(search::SearchResultItem const &item);
|
||||
};
|
||||
+26
-23
@@ -9,29 +9,14 @@
|
||||
#include <random>
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include "playlistutils.h"
|
||||
|
||||
UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=] {
|
||||
if (m_initialVideoId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
|
||||
});
|
||||
connect(this, &UserPlaylistModel::playlistIdChanged, this, [=] {
|
||||
if (m_playlistId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
|
||||
});
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [=](const watch::Playlist &playlist) {
|
||||
auto handleResult = [=, this](const watch::Playlist &playlist) {
|
||||
setLoading(false);
|
||||
|
||||
beginResetModel();
|
||||
@@ -48,8 +33,26 @@ 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(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -101,7 +104,7 @@ void UserPlaylistModel::setInitialVideoId(const QString &videoId)
|
||||
QString UserPlaylistModel::nextVideoId() const
|
||||
{
|
||||
auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
[=](const watch::Playlist::Track &track) {
|
||||
[this](const watch::Playlist::Track &track) {
|
||||
return track.video_id == m_currentVideoId.toStdString();
|
||||
});
|
||||
|
||||
@@ -129,7 +132,7 @@ void UserPlaylistModel::setCurrentVideoId(const QString &videoId)
|
||||
bool UserPlaylistModel::canSkip() const
|
||||
{
|
||||
const auto currentTrackIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
[=](const watch::Playlist::Track &track) {
|
||||
[this](const watch::Playlist::Track &track) {
|
||||
return track.video_id == m_currentVideoId.toStdString();
|
||||
});
|
||||
|
||||
@@ -157,7 +160,7 @@ void UserPlaylistModel::skipTo(const QString &videoId)
|
||||
void UserPlaylistModel::playNext(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
|
||||
{
|
||||
const auto currentIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
[=](const watch::Playlist::Track &track) {
|
||||
[this](const watch::Playlist::Track &track) {
|
||||
return track.video_id == m_currentVideoId.toStdString();
|
||||
});
|
||||
|
||||
@@ -239,7 +242,7 @@ void UserPlaylistModel::shufflePlaylist()
|
||||
// Only shuffle playlist after current track
|
||||
if (!m_currentVideoId.isEmpty()) {
|
||||
const auto currentIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
[=](const watch::Playlist::Track &track) {
|
||||
[this](const watch::Playlist::Track &track) {
|
||||
return track.video_id == m_currentVideoId.toStdString();
|
||||
});
|
||||
|
||||
@@ -257,7 +260,7 @@ void UserPlaylistModel::emitCurrentVideoChanged(const QString &oldVideoId)
|
||||
return track.video_id == oldVideoId.toStdString();
|
||||
});
|
||||
const auto currentVideoIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
[=](const watch::Playlist::Track &track) {
|
||||
[this](const watch::Playlist::Track &track) {
|
||||
return track.video_id == m_currentVideoId.toStdString();
|
||||
});
|
||||
|
||||
|
||||
+24
-21
@@ -4,12 +4,15 @@
|
||||
|
||||
#include "videoinfoextractor.h"
|
||||
|
||||
#include <asyncytmusic.h>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
#include "library.h"
|
||||
|
||||
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(this, &VideoInfoExtractor::videoIdChanged, this, [=] {
|
||||
connect(this, &VideoInfoExtractor::videoIdChanged, this, [this] {
|
||||
if (m_videoId.isEmpty()) {
|
||||
m_videoInfo = {};
|
||||
Q_EMIT audioUrlChanged();
|
||||
@@ -20,23 +23,24 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
|
||||
});
|
||||
|
||||
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [=](const video_info::VideoInfo &info) {
|
||||
m_videoInfo = info;
|
||||
setLoading(false);
|
||||
Q_EMIT audioUrlChanged();
|
||||
Q_EMIT titleChanged();
|
||||
Q_EMIT songChanged();
|
||||
Q_EMIT thumbnailChanged();
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QUrl VideoInfoExtractor::audioUrl() const
|
||||
{
|
||||
if (m_videoInfo.formats.empty()) {
|
||||
return QUrl();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<video_info::Format> audioFormats;
|
||||
@@ -48,17 +52,13 @@ QUrl VideoInfoExtractor::audioUrl() const
|
||||
});
|
||||
|
||||
if (audioFormats.empty()) {
|
||||
return QUrl();
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
struct {
|
||||
bool operator()(const video_info::Format &a, const video_info::Format &b) {
|
||||
return a.quality > b.quality;
|
||||
}
|
||||
} qualitySort;
|
||||
|
||||
std::sort(audioFormats.begin(), audioFormats.end(), qualitySort);
|
||||
std::sort(audioFormats.begin(), audioFormats.end(),
|
||||
[](const video_info::Format &a, const video_info::Format &b) {
|
||||
return a.quality > b.quality;
|
||||
});
|
||||
|
||||
return QUrl(QString::fromStdString(audioFormats.front().url));
|
||||
}
|
||||
@@ -70,6 +70,9 @@ QString VideoInfoExtractor::videoId() const
|
||||
|
||||
void VideoInfoExtractor::setVideoId(const QString &videoId)
|
||||
{
|
||||
if (m_videoId == videoId || videoId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_videoId = videoId;
|
||||
Q_EMIT videoIdChanged();
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ void pyPrintPretty(py::handle obj) {
|
||||
|
||||
#define UNEXPORT __attribute__ ((visibility("hidden")))
|
||||
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.19.4";
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.20.0";
|
||||
|
||||
struct UNEXPORT YTMusicPrivate {
|
||||
py::scoped_interpreter guard {};
|
||||
|
||||
Reference in new issue
Block a user