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 | ||
|
|
762976159e | ||
|
|
de1a872f74 | ||
|
|
3f39fc9e90 | ||
|
|
e8e6a5f1e2 | ||
|
|
51a9799281 | ||
|
|
73078eb5e7 | ||
|
|
d138b5c67b | ||
|
|
ef3846df6b | ||
|
|
a31494d83f | ||
|
|
b131fcc7d9 | ||
|
|
e6379db384 | ||
|
|
f50cf92e9a | ||
|
|
e9e0e4f143 | ||
|
|
dfc0b8cbcd | ||
|
|
473b4ee83e | ||
|
|
f40c9fd497 | ||
|
|
0ca21344f8 | ||
|
|
d4d85a8b58 | ||
|
|
cfd4b3de53 | ||
|
|
96c5f344cc | ||
|
|
d6e8ddca11 | ||
|
|
db63766cc7 | ||
|
|
048e876710 | ||
|
|
d7e74e2a5f | ||
|
|
722b78e49b | ||
|
|
734caa0280 |
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
|
||||
+4
-3
@@ -8,11 +8,11 @@ project(audiotube)
|
||||
|
||||
include(FeatureSummary)
|
||||
|
||||
set(PROJECT_VERSION "21.08")
|
||||
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)
|
||||
@@ -36,10 +36,11 @@ kde_enable_exceptions()
|
||||
find_package(Qt5 ${QT5_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg)
|
||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddons Crash)
|
||||
find_package(pybind11 REQUIRED)
|
||||
find_package(YoutubeDL REQUIRED RUNTIME)
|
||||
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
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
|
||||
execute_process(COMMAND ${Python3_EXECUTABLE} -c "import youtube_dl" RESULT_VARIABLE YTDL_CHECK_RESULT)
|
||||
execute_process(COMMAND ${Python3_EXECUTABLE} -c "import yt_dlp" RESULT_VARIABLE YTDLP_CHECK_RESULT)
|
||||
|
||||
if (${YTDL_CHECK_RESULT} EQUAL 0)
|
||||
set(YoutubeDL_FOUND TRUE)
|
||||
if (${YTDLP_CHECK_RESULT} EQUAL 0)
|
||||
set(Ytdlp_FOUND TRUE)
|
||||
endif()
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "python3-youtube_dl",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"youtube_dl\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/c6/75/05979677d9abc76851d13d8db3951e39017ac223545adab6e8576fa0cbe7/youtube_dl-2021.6.6.tar.gz",
|
||||
"sha256": "cb2d3ee002158ede783e97a82c95f3817594df54367ea6a77ce5ceea4772f0ab"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "python3-yt-dlp",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"yt-dlp\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"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/f8/a3/622d9acbfb9a71144b5d7609906bc648c62e3ca5fdbb1c8cca222949d82c/websockets-10.3.tar.gz",
|
||||
"sha256": "fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
File renamed without changes.
@@ -7,33 +7,33 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/4f/5a/597ef5911cb8919efe4d86206aa8b2658616d676a7088f0825ca08bd7cb8/urllib3-1.26.6.tar.gz",
|
||||
"sha256": "f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
"url": "https://files.pythonhosted.org/packages/cb/58/834a6445479c72544d9285badce0a58650520c8ba5adfae7b98e61d133bb/ytmusicapi-0.21.0-py3-none-any.whl",
|
||||
"sha256": "29d8d44e1911458041882a453b05c37021ff850757c648f93a45d1775dddbae2"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz",
|
||||
"sha256": "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"
|
||||
"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/ee/2d/9cdc2b527e127b4c9db64b86647d567985940ac3698eeabc7ffaccb4ea61/chardet-4.0.0.tar.gz",
|
||||
"sha256": "0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"
|
||||
"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/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz",
|
||||
"sha256": "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"
|
||||
"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/6b/47/c14abc08432ab22dc18b9892252efaf005ab44066de871e72a38d6af464b/requests-2.25.1.tar.gz",
|
||||
"sha256": "27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"
|
||||
"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/83/aa/3e2c231fbef90d5bd75614e3739bb27e7e33e28c9891d6ee0561fe52a9df/ytmusicapi-0.18.0.tar.gz",
|
||||
"sha256": "447d1b6a594f49942f7ac75412cba59c191b6f3540d98ac65a1333445beec16d"
|
||||
"url": "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
|
||||
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,5 +16,5 @@ else
|
||||
git -C flatpak-builder-tools pull
|
||||
fi
|
||||
|
||||
./flatpak-builder-tools/pip/flatpak-pip-generator youtube_dl
|
||||
./flatpak-builder-tools/pip/flatpak-pip-generator yt-dlp
|
||||
./flatpak-builder-tools/pip/flatpak-pip-generator ytmusicapi
|
||||
@@ -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,14 +25,17 @@ 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="ca">Client convergent per a la música de YouTube</summary>
|
||||
<summary xml:lang="ca-valencia">Client convergent per a la música de YouTube</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>
|
||||
<summary xml:lang="en-GB">Convergent YouTube Music client</summary>
|
||||
<summary xml:lang="es">Cliente de música de YouTube convergente</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,27 @@ 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>
|
||||
<release version="21.08" date="2021-08-31">
|
||||
<url>https://www.plasma-mobile.org/2021/08/31/plasma-mobile-gear-21-08/</url>
|
||||
</release>
|
||||
@@ -97,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>
|
||||
@@ -116,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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "org.kde.audiotube",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "5.15",
|
||||
"runtime-version": "5.15-21.08",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "audiotube",
|
||||
"tags": ["nightly"],
|
||||
@@ -21,14 +21,14 @@
|
||||
],
|
||||
|
||||
"modules": [
|
||||
"flatpak/python3-youtube_dl.json",
|
||||
"flatpak/python3-yt-dlp.json",
|
||||
"flatpak/python3-ytmusicapi.json",
|
||||
{
|
||||
"name": "pybind11",
|
||||
"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.18.0
|
||||
youtube_dl
|
||||
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)
|
||||
|
||||
|
||||
+16
-20
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -43,12 +44,7 @@ QVariant AlbumModel::data(const QModelIndex &index, int role) const
|
||||
case VideoId:
|
||||
return QString::fromStdString(m_album.tracks[index.row()].video_id.value_or(std::string()));
|
||||
case Artists:
|
||||
return QVariant::fromValue(std::vector<meta::Artist> {
|
||||
{
|
||||
m_album.tracks[index.row()].artists,
|
||||
std::nullopt
|
||||
}
|
||||
});
|
||||
return QVariant::fromValue(m_album.tracks[index.row()].artists);
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
@@ -92,5 +88,5 @@ QUrl AlbumModel::thumbnailUrl() const
|
||||
|
||||
QString AlbumModel::playlistId() const
|
||||
{
|
||||
return QString::fromStdString(m_album.playlist_id);
|
||||
return QString::fromStdString(m_album.audio_playlist_id);
|
||||
}
|
||||
+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;
|
||||
};
|
||||
+50
-101
@@ -6,6 +6,9 @@
|
||||
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFutureInterface>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
@@ -13,10 +16,21 @@
|
||||
|
||||
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()) {
|
||||
return op(optional.value());
|
||||
if constexpr (std::is_member_function_pointer<OP>::value) {
|
||||
return (&optional.value()->*op)();
|
||||
} else {
|
||||
return op(optional.value());
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
@@ -37,15 +51,6 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
|
||||
qRegisterMetaType<std::vector<meta::Artist>>();
|
||||
qRegisterMetaType<meta::Artist>();
|
||||
|
||||
connect(this, &AsyncYTMusic::startSearch, this, &AsyncYTMusic::internalSearch);
|
||||
connect(this, &AsyncYTMusic::startFetchArtist, this, &AsyncYTMusic::internalFetchArtist);
|
||||
connect(this, &AsyncYTMusic::startFetchAlbum, this, &AsyncYTMusic::internalFetchAlbum);
|
||||
connect(this, &AsyncYTMusic::startFetchSong, this, &AsyncYTMusic::internalFetchSong);
|
||||
connect(this, &AsyncYTMusic::startFetchPlaylist, this, &AsyncYTMusic::internalFetchPlaylist);
|
||||
connect(this, &AsyncYTMusic::startFetchArtistAlbums, this, &AsyncYTMusic::internalFetchArtistAlbums);
|
||||
connect(this, &AsyncYTMusic::startExtractVideoInfo, this, &AsyncYTMusic::internalExtractVideoInfo);
|
||||
connect(this, &AsyncYTMusic::startFetchWatchPlaylist, this, &AsyncYTMusic::internalFetchWatchPlaylist);
|
||||
|
||||
connect(this, &AsyncYTMusic::errorOccurred, this, [](const QString &err) {
|
||||
std::cerr << qPrintable(err);
|
||||
});
|
||||
@@ -54,139 +59,83 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
|
||||
//
|
||||
// search
|
||||
//
|
||||
void AsyncYTMusic::search(const QString &query)
|
||||
QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QString &query)
|
||||
{
|
||||
Q_EMIT startSearch(query);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalSearch(const QString &query)
|
||||
{
|
||||
try {
|
||||
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startFetchArtist(channelId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchArtist(const QString &channelId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startFetchAlbum(browseId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchAlbum(const QString &browseId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startFetchSong(videoId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchSong(const QString &videoId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchSongFinished(m_ytm->get_song(videoId.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
return invokeAndCatchOnThread<std::optional<song::Song>>([=, this]() {
|
||||
return m_ytm->get_song(videoId.toStdString());
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// fetchPlaylist
|
||||
//
|
||||
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
|
||||
Q_EMIT startFetchPlaylist(playlistId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchPlaylist(const QString &playlistId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startFetchArtistAlbums(channelId, params);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchArtistAlbums(const QString &channelid, const QString ¶ms)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelid.toStdString(), params.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startExtractVideoInfo(videoId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalExtractVideoInfo(const QString &videoId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
{
|
||||
Q_EMIT startFetchWatchPlaylist(videoId, playlistId);
|
||||
}
|
||||
|
||||
void AsyncYTMusic::internalFetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
|
||||
{
|
||||
try {
|
||||
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
|
||||
map_optional<std::string>(videoId, [](const QString &value) { return value.toStdString(); }),
|
||||
map_optional<std::string>(playlistId, [](const QString &value) { return value.toStdString(); })
|
||||
));
|
||||
} catch (const py::error_already_set &err) {
|
||||
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
|
||||
}
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
YTMusicThread &YTMusicThread::instance()
|
||||
|
||||
+37
-39
@@ -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);
|
||||
|
||||
@@ -84,29 +89,22 @@ protected:
|
||||
explicit AsyncYTMusic(QObject *parent = nullptr);
|
||||
|
||||
private:
|
||||
Q_SIGNAL void startSearch(const QString &query);
|
||||
Q_SLOT void internalSearch(const QString &query);
|
||||
|
||||
Q_SIGNAL void startFetchArtist(const QString &channelId);
|
||||
Q_SLOT void internalFetchArtist(const QString &channelId);
|
||||
|
||||
Q_SIGNAL void startFetchAlbum(const QString &browseId);
|
||||
Q_SLOT void internalFetchAlbum(const QString &browseId);
|
||||
|
||||
Q_SIGNAL void startFetchSong(const QString &videoId);
|
||||
Q_SLOT void internalFetchSong(const QString &videoId);
|
||||
|
||||
Q_SIGNAL void startFetchPlaylist(const QString &playlistId);
|
||||
Q_SLOT void internalFetchPlaylist(const QString &playlistId);
|
||||
|
||||
Q_SIGNAL void startFetchArtistAlbums(const QString &channelid, const QString ¶ms);
|
||||
Q_SLOT void internalFetchArtistAlbums(const QString &channelid, const QString ¶ms);
|
||||
|
||||
Q_SIGNAL void startExtractVideoInfo(const QString &videoId);
|
||||
Q_SLOT void internalExtractVideoInfo(const QString &videoId);
|
||||
|
||||
Q_SIGNAL void startFetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId);
|
||||
Q_SLOT void internalFetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId);
|
||||
/// 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();
|
||||
}
|
||||
|
||||
// Python interpreter will be initialized from the thread calling the methods
|
||||
Lazy<YTMusic> m_ytm;
|
||||
|
||||
@@ -36,7 +36,15 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
onOpenVideo: (videoId, title) => {
|
||||
askPlayVideoAudio(videoId, title)
|
||||
videoPlayMessage.text = i18n("Video playback is not supported yet. Do you want to play only the audio of \"%1\"?", title)
|
||||
videoPlayMessage.visible = true
|
||||
videoPlayMessage.okAction = function() {
|
||||
play(videoId)
|
||||
videoPlayMessage.visible = false
|
||||
}
|
||||
videoPlayMessage.cancelAction = function() {
|
||||
videoPlayMessage.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
section.property: "type"
|
||||
@@ -110,4 +118,8 @@ Kirigami.ScrollablePage {
|
||||
visible: artistModel.loading
|
||||
}
|
||||
}
|
||||
|
||||
footer: ConfirmationMessage {
|
||||
id: videoPlayMessage
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2021 Dimitris Kardarakos <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
|
||||
property var okAction
|
||||
property var cancelAction
|
||||
|
||||
visible: false
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("OK")
|
||||
|
||||
onTriggered: okAction()
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Cancel")
|
||||
|
||||
onTriggered: cancelAction()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -223,6 +226,7 @@ Item {
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,27 @@ Kirigami.ScrollablePage {
|
||||
ListView {
|
||||
reuseItems: true
|
||||
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
required property string section
|
||||
text: {
|
||||
switch (parseInt(section)) {
|
||||
case SearchModel.Album:
|
||||
return i18n("Albums")
|
||||
case SearchModel.Artist:
|
||||
return i18n("Artists")
|
||||
case SearchModel.Playlist:
|
||||
return i18n("Playlists")
|
||||
case SearchModel.Song:
|
||||
return i18n("Songs")
|
||||
case SearchModel.Video:
|
||||
return i18n("Videos")
|
||||
}
|
||||
|
||||
return i18n("Unknown")
|
||||
}
|
||||
}
|
||||
section.property: "type"
|
||||
|
||||
model: SearchModel {
|
||||
id: searchModel
|
||||
|
||||
@@ -43,7 +64,15 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
onOpenVideo: (videoId, title) => {
|
||||
askPlayVideoAudio(videoId, title)
|
||||
videoPlayMessage.text = i18n("Video playback is not supported yet. Do you want to play only the audio of \"%1\"?", title)
|
||||
videoPlayMessage.visible = true
|
||||
videoPlayMessage.okAction = function() {
|
||||
play(videoId)
|
||||
videoPlayMessage.visible = false
|
||||
}
|
||||
videoPlayMessage.cancelAction = function() {
|
||||
videoPlayMessage.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
@@ -86,13 +115,13 @@ Kirigami.ScrollablePage {
|
||||
Kirigami.Action {
|
||||
icon.name: "go-next"
|
||||
text: i18n("Play next")
|
||||
visible: delegateItem.type === SearchModel.Song
|
||||
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
|
||||
onTriggered: UserPlaylistModel.playNext(delegateItem.videoId, delegateItem.title, delegateItem.artists)
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "media-playlist-append"
|
||||
text: i18n("Add to Playlist")
|
||||
visible: delegateItem.type === SearchModel.Song
|
||||
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
|
||||
onTriggered: UserPlaylistModel.append(delegateItem.videoId, delegateItem.title, delegateItem.artists)
|
||||
},
|
||||
Kirigami.Action {
|
||||
@@ -123,4 +152,8 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: ConfirmationMessage {
|
||||
id: videoPlayMessage
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -71,14 +110,6 @@ Kirigami.ApplicationWindow {
|
||||
UserPlaylistModel.playlistId = playlistId
|
||||
}
|
||||
|
||||
function askPlayVideoAudio(videoId, title) {
|
||||
showPassiveNotification(i18n("Video playback is not supported yet. Do you want to play only the audio of \"%1\"?", title),
|
||||
"long",
|
||||
i18n("Play only Audio"),
|
||||
function() {
|
||||
play(videoId)
|
||||
})
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ErrorHandler
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
int main() {
|
||||
const auto ytm = YTMusic();
|
||||
const auto results = ytm.search("Thomas Bangalter");
|
||||
const auto results = ytm.search("Another one bites the dust");
|
||||
|
||||
std::cout << "Found " << results.size() << " results." << std::endl;
|
||||
|
||||
|
||||
+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 {};
|
||||
|
||||
+2
-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>
|
||||
@@ -12,5 +6,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<file alias="PlayerFooter.qml">contents/ui/PlayerFooter.qml</file>
|
||||
<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();
|
||||
});
|
||||
|
||||
|
||||
+25
-22
@@ -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();
|
||||
}
|
||||
@@ -81,7 +84,7 @@ QString VideoInfoExtractor::title() const
|
||||
|
||||
QString VideoInfoExtractor::thumbnail() const
|
||||
{
|
||||
return QString::fromStdString(m_videoInfo.thumnnail);
|
||||
return QString::fromStdString(m_videoInfo.thumbnail);
|
||||
}
|
||||
|
||||
bool VideoInfoExtractor::loading() const
|
||||
|
||||
+51
-43
@@ -13,9 +13,15 @@ namespace py = pybind11;
|
||||
|
||||
using namespace py::literals;
|
||||
|
||||
/// Useful for debugging
|
||||
void pyPrintPretty(py::handle obj) {
|
||||
auto json = py::module::import("json");
|
||||
py::print(json.attr("dumps")(obj, "indent"_a=py::int_(4)));
|
||||
}
|
||||
|
||||
#define UNEXPORT __attribute__ ((visibility("hidden")))
|
||||
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.18.0";
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.20.0";
|
||||
|
||||
struct UNEXPORT YTMusicPrivate {
|
||||
py::scoped_interpreter guard {};
|
||||
@@ -47,7 +53,7 @@ struct UNEXPORT YTMusicPrivate {
|
||||
py::object get_ytdl() {
|
||||
// lazy initialization
|
||||
if (ytdl.is_none()) {
|
||||
ytdl = py::module::import("youtube_dl").attr("YoutubeDL")(py::dict());
|
||||
ytdl = py::module::import("yt_dlp").attr("YoutubeDL")(py::dict());
|
||||
}
|
||||
|
||||
return ytdl;
|
||||
@@ -64,7 +70,7 @@ std::optional<T> optional_key(py::handle obj, const char *name) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return obj[name].cast<T>();
|
||||
return obj[name].cast<std::optional<T>>();
|
||||
}
|
||||
|
||||
meta::Thumbnail extract_thumbnail(py::handle thumbnail) {
|
||||
@@ -82,28 +88,10 @@ meta::Artist extract_meta_artist(py::handle artist) {
|
||||
};
|
||||
};
|
||||
|
||||
album::Track extract_album_track(py::handle track) {
|
||||
return {
|
||||
track["index"].cast<std::string>(),
|
||||
track["title"].cast<std::string>(),
|
||||
track["artists"].cast<std::string>(),
|
||||
track["videoId"].cast<std::optional<std::string>>(), // E rated songs don't have a videoId
|
||||
track["lengthMs"].cast<std::optional<std::string>>(), //
|
||||
track["likeStatus"].cast<std::optional<std::string>>()
|
||||
};
|
||||
}
|
||||
|
||||
video_info::Format extract_format(py::handle format) {
|
||||
return {
|
||||
optional_key<int>(format, "quality"),
|
||||
format["url"].cast<std::string>(),
|
||||
format["vcodec"].cast<std::string>(),
|
||||
format["acodec"].cast<std::string>()
|
||||
};
|
||||
}
|
||||
|
||||
playlist::Track extract_playlist_track(py::handle track);
|
||||
watch::Playlist::Track extract_watch_track(py::handle track);
|
||||
album::Track extract_album_track(py::handle track);
|
||||
video_info::Format extract_format(py::handle format);
|
||||
|
||||
template <typename T>
|
||||
inline auto extract_py_list(py::handle obj) {
|
||||
@@ -135,6 +123,27 @@ inline auto extract_py_list(py::handle obj) {
|
||||
return output;
|
||||
}
|
||||
|
||||
album::Track extract_album_track(py::handle track) {
|
||||
return {
|
||||
track["isExplicit"].cast<bool>(),
|
||||
track["title"].cast<std::string>(),
|
||||
extract_py_list<meta::Artist>(track["artists"]),
|
||||
track["album"].cast<std::optional<std::string>>(),
|
||||
track["videoId"].cast<std::optional<std::string>>(), // E rated songs don't have a videoId
|
||||
track["duration"].cast<std::optional<std::string>>(), //
|
||||
track["likeStatus"].cast<std::optional<std::string>>()
|
||||
};
|
||||
}
|
||||
|
||||
video_info::Format extract_format(py::handle format) {
|
||||
return {
|
||||
optional_key<int>(format, "quality"),
|
||||
format["url"].cast<std::string>(),
|
||||
format["vcodec"].cast<std::string>(),
|
||||
format["acodec"].cast<std::string>()
|
||||
};
|
||||
}
|
||||
|
||||
meta::Album extract_meta_album(py::handle album) {
|
||||
return meta::Album {
|
||||
album["name"].cast<std::string>(),
|
||||
@@ -280,7 +289,7 @@ std::optional<search::SearchResultItem> extract_search_result(py::handle result)
|
||||
result["title"].cast<std::string>(),
|
||||
result["type"].cast<std::string>(),
|
||||
extract_py_list<meta::Artist>(result["artists"]),
|
||||
result["year"].cast<std::string>(),
|
||||
result["year"].cast<std::optional<std::string>>(),
|
||||
result["isExplicit"].cast<bool>()
|
||||
};
|
||||
} else if (resultType == "playlist") {
|
||||
@@ -299,7 +308,8 @@ std::optional<search::SearchResultItem> extract_search_result(py::handle result)
|
||||
};
|
||||
} else {
|
||||
std::cerr << "Warning: Unsupported search result type found" << std::endl;
|
||||
std::cerr << "It's called: " << resultType;
|
||||
std::cerr << "It's called: " << resultType << std::endl;
|
||||
pyPrintPretty(result);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -324,10 +334,11 @@ YTMusic::~YTMusic() = default;
|
||||
std::vector<search::SearchResultItem> YTMusic::search(
|
||||
const std::string &query,
|
||||
const std::optional<std::string> &filter,
|
||||
const std::optional<std::string> &scope,
|
||||
const int limit,
|
||||
const bool ignore_spelling) const
|
||||
{
|
||||
const auto results = d->get_ytmusic().attr("search")(query, filter, limit, ignore_spelling).cast<py::list>();
|
||||
const auto results = d->get_ytmusic().attr("search")("query"_a=query, "filter"_a=filter, "scope"_a=scope, "limit"_a = limit, "ignore_spelling"_a = ignore_spelling).cast<py::list>();
|
||||
|
||||
std::vector<search::SearchResultItem> output;
|
||||
for (const auto &result : results) {
|
||||
@@ -343,7 +354,7 @@ artist::Artist YTMusic::get_artist(const std::string &channel_id) const
|
||||
{
|
||||
const auto artist = d->get_ytmusic().attr("get_artist")(channel_id);
|
||||
return artist::Artist {
|
||||
artist["description"].cast<std::optional<std::string>>(),
|
||||
optional_key<std::string>(artist, "description"),
|
||||
artist["views"].cast<std::optional<std::string>>(),
|
||||
artist["name"].cast<std::string>(),
|
||||
artist["channelId"].cast<std::string>(),
|
||||
@@ -360,31 +371,28 @@ artist::Artist YTMusic::get_artist(const std::string &channel_id) const
|
||||
album::Album YTMusic::get_album(const std::string &browseId) const
|
||||
{
|
||||
const auto album = d->get_ytmusic().attr("get_album")(browseId);
|
||||
|
||||
return {
|
||||
album["title"].cast<std::string>(),
|
||||
album["trackCount"].cast<std::string>(),
|
||||
album["durationMs"].cast<std::string>(),
|
||||
album["playlistId"].cast<std::string>(),
|
||||
[&]() -> album::Album::ReleaseDate {
|
||||
const auto date = album["releaseDate"];
|
||||
return {
|
||||
date["year"].cast<int>(),
|
||||
date["month"].cast<int>(),
|
||||
date["day"].cast<int>()
|
||||
};
|
||||
}(),
|
||||
album["description"].cast<std::string>(),
|
||||
album["trackCount"].cast<int>(),
|
||||
album["duration"].cast<std::string>(),
|
||||
album["audioPlaylistId"].cast<std::string>(),
|
||||
album["year"].cast<std::string>(),
|
||||
optional_key<std::string>(album, "description"),
|
||||
extract_py_list<meta::Thumbnail>(album["thumbnails"]),
|
||||
extract_py_list<album::Track>(album["tracks"])
|
||||
extract_py_list<album::Track>(album["tracks"]),
|
||||
extract_py_list<meta::Artist>(album["artists"])
|
||||
};
|
||||
}
|
||||
|
||||
song::Song YTMusic::get_song(const std::string &video_id) const
|
||||
std::optional<song::Song> YTMusic::get_song(const std::string &video_id) const
|
||||
{
|
||||
const auto song = d->get_ytmusic().attr("get_song")(video_id);
|
||||
|
||||
return {
|
||||
if (!song.cast<py::dict>().contains("videoId")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return song::Song {
|
||||
song["videoId"].cast<std::string>(),
|
||||
song["title"].cast<std::string>(),
|
||||
song["lengthSeconds"].cast<std::string>(),
|
||||
|
||||
+15
-12
@@ -67,7 +67,7 @@ struct Album {
|
||||
std::string title;
|
||||
std::string type;
|
||||
std::vector<meta::Artist> artists;
|
||||
std::string year;
|
||||
std::optional<std::string> year;
|
||||
bool is_explicit;
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ struct Artist {
|
||||
|
||||
struct Single {
|
||||
std::string title;
|
||||
std::vector<meta::Thumbnail> thumnnails;
|
||||
std::vector<meta::Thumbnail> thumbnails;
|
||||
std::string year;
|
||||
std::string browse_id;
|
||||
};
|
||||
@@ -143,11 +143,12 @@ struct Artist {
|
||||
|
||||
namespace album {
|
||||
struct Track {
|
||||
std::string index;
|
||||
bool is_explicit;
|
||||
std::string title;
|
||||
std::string artists;
|
||||
std::vector<meta::Artist> artists;
|
||||
std::optional<std::string> album;
|
||||
std::optional<std::string> video_id;
|
||||
std::optional<std::string> length_ms;
|
||||
std::optional<std::string> duration;
|
||||
std::optional<std::string> like_status;
|
||||
};
|
||||
|
||||
@@ -159,13 +160,14 @@ namespace album {
|
||||
};
|
||||
|
||||
std::string title;
|
||||
std::string track_count;
|
||||
std::string duration_ms;
|
||||
std::string playlist_id;
|
||||
ReleaseDate release_date;
|
||||
std::string description;
|
||||
int track_count;
|
||||
std::string duration;
|
||||
std::string audio_playlist_id;
|
||||
std::string year;
|
||||
std::optional<std::string> description;
|
||||
std::vector<meta::Thumbnail> thumbnails;
|
||||
std::vector<Track> tracks;
|
||||
std::vector<meta::Artist> artists;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -240,7 +242,7 @@ struct VideoInfo {
|
||||
std::string id;
|
||||
std::string title;
|
||||
std::vector<Format> formats;
|
||||
std::string thumnnail;
|
||||
std::string thumbnail;
|
||||
|
||||
// More, but not interesting for us right now
|
||||
};
|
||||
@@ -277,6 +279,7 @@ public:
|
||||
|
||||
std::vector<search::SearchResultItem> search(const std::string &query,
|
||||
const std::optional<std::string> &filter = std::nullopt,
|
||||
const std::optional<std::string> &scope = std::nullopt,
|
||||
const int limit = 20,
|
||||
const bool ignore_spelling = false) const;
|
||||
|
||||
@@ -287,7 +290,7 @@ public:
|
||||
album::Album get_album(const std::string &browse_id) const;
|
||||
|
||||
/// https://ytmusicapi.readthedocs.io/en/latest/reference.html#ytmusicapi.YTMusic.get_song
|
||||
song::Song get_song(const std::string &video_id) const;
|
||||
std::optional<song::Song> get_song(const std::string &video_id) const;
|
||||
|
||||
/// https://ytmusicapi.readthedocs.io/en/latest/reference.html#ytmusicapi.YTMusic.get_playlist
|
||||
playlist::Playlist get_playlist(const std::string &playlist_id, int limit = 100) const;
|
||||
|
||||
Reference in new issue
Block a user