Compare commits

..
Author SHA1 Message Date
Jonah Brüchert 5112d0bd1b MultiIterableView: Add convinience std::vector constructor 2022-02-14 17:58:55 +01:00
Dimitris Kardarakos 2b60ae8745 Power search page list sections
The Search Page list view can present content in sections according to media type (artist, playlist, song, video). For sections to work, the model that provides the search results should sort the results accordingly.
2022-02-14 17:25:41 +01:00
Jonah Brüchert 15f439c38e Simplify std::sort use 2022-02-14 17:21:34 +01:00
Jonah Brüchert ca4d5d8b77 MultiIterableView: Add documentation 2022-02-14 17:17:12 +01:00
Jonah Brüchert 06338900dc Fix some implicit this captures 2022-02-14 17:14:46 +01:00
Jonah Brüchert 8ec674fbef MultiIterableView: Generalize by using std::span
Requires upgrading to C++20
2022-02-14 17:10:06 +01:00
Jonah Brüchert 58eefc29eb ArtistModel: Simplify use of MultiIterableView
Now that it is truly a view type, it is possible to just modify the
underlying data, instead of recreating the view.
This allows to get rid of the optional, in which the view was previously
stored.
2022-02-14 15:52:25 +01:00
Jonah Brüchert 9312331755 MultiIterableView: Refactor into an actual view type 2022-02-14 15:52:25 +01:00
Jonah Brüchert 1d25f46f52 MultiIterableView: Try to make code easier to understand 2022-02-14 15:52:22 +01:00
Jonah Brüchert a775cd58be flatpak: Update pybind11 2022-02-13 20:16:18 +01:00
Bhushan Shah ae5ed3b10b GIT_SILENT: fixup typo 22.02 2022-02-08 20:58:24 +05:30
Bhushan Shah 8aab91568e GIT_SILENT: Update version and appstream data for 22.02 2022-02-08 18:23:50 +05:30
l10n daemon script 6647346314 GIT_SILENT made messages (after extraction) 2022-02-06 00:49:47 +00:00
Jonah Brüchert bfb41310ad update ytmusicapi 2022-01-22 13:50:40 +01:00
l10n daemon script c3371adda4 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-01-22 01:50:01 +00:00
l10n daemon script 9bcf017c8d GIT_SILENT made messages (after extraction) 2022-01-22 00:46:14 +00:00
Ml Ge 2c0b13ff9f minor readme and gitignore updates 2022-01-09 19:47:57 +01:00
Ml Ge abfd901a82 replace .license files with entries in dep5 2022-01-09 19:47:33 +01:00
Jonah Brüchert 135b8859d6 Fix typo in requirements.txt 2021-12-31 18:43:16 +01:00
Jonah Brüchert f8637b686c flatpak: Update yt-dlp 2021-12-31 18:17:32 +01:00
Jonah Brüchert 53f466c976 Update tested ytmusicapi version to 0.19.5 2021-12-21 12:53:38 +01:00
Bhushan Shah 762976159e appdata: add changelog for 21.12 2021-12-05 19:39:47 +05:30
Bhushan Shah de1a872f74 GIT_SILENT Update version number for 21.12 2021-12-05 10:15:46 +05:30
Dimitris Kardarakos 3f39fc9e90 Substitute video-play passive notification with an inline message
On mobile, the action of the video play notification was not properly shown. On top of that, an inline message seems more suitable for this purpose. An explicit cancel action has been added as well; previously it was accomplished by ignoring the notification.
2021-12-03 15:43:00 +02:00
Jonah Brüchert e8e6a5f1e2 Update some more places to ytdlp 2021-11-29 23:21:32 +01:00
Jonah Brüchert 51a9799281 Switch to yt-dlp 2021-11-29 01:43:37 +01:00
Jonah Brüchert 73078eb5e7 Commit forgotten changes 2021-11-11 12:00:51 +01:00
Jonah Brüchert d138b5c67b Further reduce amount of code of the async YTMusic wrapper 2021-11-11 12:00:16 +01:00
Jonah Brüchert ef3846df6b More awesome map_optional function 2021-11-11 11:40:27 +01:00
Jonah Brüchert a31494d83f Simplify multithreading 2021-11-11 11:22:43 +01:00
Jonah Brüchert b131fcc7d9 Make the progress bar smooth
The position update seems to come every second, so an animation duration of a second should produce a smooth progress.
2021-11-11 11:02:23 +01:00
Jonah Brüchert e6379db384 Update tested ytmusicapi version to 0.19.4 2021-11-04 22:36:17 +01:00
Jonah Brüchert f50cf92e9a flatpak: Update python packages 2021-10-27 00:37:12 +02:00
Jonah Brüchert e9e0e4f143 Fix typo 2021-09-30 01:08:06 +02:00
Jonah Brüchert dfc0b8cbcd Fix extracting album tracks with ytmusicapi 0.19.3 2021-09-30 00:02:03 +02:00
Jonah Brüchert 473b4ee83e Update flatpak runtime 2021-09-29 23:46:25 +02:00
Jonah Brüchert f40c9fd497 Update ytmusicapi 2021-09-29 23:32:36 +02:00
Jonah Brüchert 0ca21344f8 Fix some crashes 2021-09-23 21:58:18 +02:00
l10n daemon script d4d85a8b58 GIT_SILENT made messages (after extraction) 2021-09-22 00:20:11 +00:00
l10n daemon script cfd4b3de53 GIT_SILENT made messages (after extraction) 2021-09-20 00:21:06 +00:00
Jonah Brüchert 96c5f344cc Also offer playlist actions for videos 2021-09-06 23:26:54 +02:00
Jonah Brüchert d6e8ddca11 get_artist: Make description key optional 2021-09-04 21:35:35 +02:00
Jonah Brüchert db63766cc7 Make optional_key do the right thing even if the key exists but is None 2021-09-04 21:34:57 +02:00
Jonah Brüchert 048e876710 Add debug output for search results that could not be understood 2021-09-03 20:11:08 +02:00
Jonah Brüchert d7e74e2a5f SearchPage: Add section headers 2021-09-03 20:05:44 +02:00
Jonah Brüchert 722b78e49b Make album description optional 2021-09-03 19:40:27 +02:00
Jonah Brüchert 734caa0280 Fix compatibility with ytmusicapi 0.19.1 2021-09-03 17:39:50 +02:00
40 changed files with 426 additions and 364 deletions

No files matched your search

+1
View File
@@ -6,3 +6,4 @@
*.kdev4
CMakeLists.txt.user
flatpak/flatpak-builder-tools/
build/
+21
View File
@@ -2,3 +2,24 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: AudioTube
Upstream-Contact: Jonah Brüchert <[email protected]>
Source: https://invent.kde.org/plasma-mobile/audiotube
# Sample paragraph, commented out:
#
# Files: ...
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# Comment: ...
Files: README.md
Copyright: 2021 Jonah Brüchert <[email protected]>
License: CC0-1.0
Files: org.kde.audiotube.desktop
org.kde.audiotube.json
Copyright: 2021 Jonah Brüchert <[email protected]>
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
Files: logo.png
org.kde.audiotube.svg
Copyright: 2021 Mathis Brüchert <[email protected]>
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+3 -3
View File
@@ -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,7 +36,7 @@ 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)
+1 -1
View File
@@ -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.
-3
View File
@@ -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()
-14
View File
@@ -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"
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"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/8e/10/7b43c0a779d4c6331088d64af7e4a2da27b47817f558e97ebbadfc41869f/yt_dlp-2022.1.21-py2.py3-none-any.whl",
"sha256": "182903b78a4645edd504dcd9f2603f76f9fdaf8bd279fd45d01992f7637b6d8a"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f6/06/e2ad9e93210790be86d36c6e2d5524ba54928c3ed27dd0be9b2ced7c57f1/pycryptodomex-3.12.0.zip",
"sha256": "922e9dac0166e4617e5c7980d2cff6912a6eb5cb5c13e7ece222438650bd7f66"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/69/77/591bbc51a5ed6a906a7813e60a9627f988f9546513fcf9d250eb31ec8689/websockets-10.1.tar.gz",
"sha256": "181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/16/b3/f7aa8edf2ff4495116f95fd442b2a346aa55d1d46313143c8814886dbcdb/mutagen-1.45.1-py3-none-any.whl",
"sha256": "9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"
}
]
}
File renamed without changes.
+12 -12
View File
@@ -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/33/d9/d18f3827dfd54acc48f29337059197514ad656ffc88e02a51e0457c5117a/ytmusicapi-0.20.0-py3-none-any.whl",
"sha256": "abdb90426baa14a7929ec810d65c1df228661b61d580678fc48c087909a9aea2"
},
{
"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/84/3e/1037abe6498e65d645ce7a22d3402605d49a3b2c7f20c3abb027760da4f0/charset_normalizer-2.0.10-py3-none-any.whl",
"sha256": "cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
},
{
"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/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl",
"sha256": "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
},
{
"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/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl",
"sha256": "84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"
},
{
"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/4e/b8/f5a25b22e803f0578e668daa33ba3701bb37858ec80e08a150bd7d2cf1b1/urllib3-1.26.8-py2.py3-none-any.whl",
"sha256": "000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"
}
]
}
+1 -1
View File
@@ -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
-3
View File
@@ -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
+20 -2
View File
@@ -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,16 @@ 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>
<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,6 +48,7 @@ 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>
@@ -52,6 +56,7 @@ SPDX-License-Identifier: CC0-1.0
<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,6 +74,7 @@ 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>
@@ -77,6 +83,17 @@ SPDX-License-Identifier: CC0-1.0
<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 +114,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>
+2
View File
@@ -1,5 +1,6 @@
[Desktop Entry]
Name=AudioTube
Name[ar]=تيوب الصوتي
Name[ca]=AudioTube
Name[ca@valencia]=AudioTube
Name[cs]=AudioTube
@@ -24,6 +25,7 @@ Name[sv]=AudioTube
Name[uk]=AudioTube
Name[x-test]=xxAudioTubexx
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
-3
View File
@@ -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
+3 -3
View File
@@ -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",
-3
View File
@@ -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
-3
View File
@@ -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
+2 -2
View File
@@ -3,5 +3,5 @@
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
ytmusicapi==0.18.0
youtube_dl
ytmusicapi==0.20.0
yt-dlp
+5 -10
View File
@@ -9,11 +9,11 @@
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) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [this](const album::Album &album) {
setLoading(false);
beginResetModel();
@@ -25,7 +25,7 @@ AlbumModel::AlbumModel(QObject *parent)
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 +43,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 +87,5 @@ QUrl AlbumModel::thumbnailUrl() const
QString AlbumModel::playlistId() const
{
return QString::fromStdString(m_album.playlist_id);
return QString::fromStdString(m_album.audio_playlist_id);
}
+28 -36
View File
@@ -8,8 +8,9 @@
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;
}
@@ -18,44 +19,37 @@ ArtistModel::ArtistModel(QObject *parent)
YTMusicThread::instance()->fetchArtist(m_channelId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [=](const artist::Artist &artist) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [this](const artist::Artist &artist) {
setLoading(false);
beginResetModel();
m_artist = artist;
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
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>();
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>();
m_view = std::optional(MultiIterableView(std::move(albums),
std::move(singles),
std::move(songs),
std::move(videos)));
// 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::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
View File
@@ -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;
};
+38 -86
View File
@@ -16,7 +16,11 @@ namespace py = pybind11;
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 +41,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);
});
@@ -56,16 +51,9 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
//
void AsyncYTMusic::search(const QString &query)
{
Q_EMIT startSearch(query);
}
void AsyncYTMusic::internalSearch(const QString &query)
{
try {
invokeAndCatchOnThread([=, this]() {
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
//
@@ -73,16 +61,9 @@ void AsyncYTMusic::internalSearch(const QString &query)
//
void AsyncYTMusic::fetchArtist(const QString &channelId)
{
Q_EMIT startFetchArtist(channelId);
}
void AsyncYTMusic::internalFetchArtist(const QString &channelId)
{
try {
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
//
@@ -90,16 +71,9 @@ void AsyncYTMusic::internalFetchArtist(const QString &channelId)
//
void AsyncYTMusic::fetchAlbum(const QString &browseId)
{
Q_EMIT startFetchAlbum(browseId);
}
void AsyncYTMusic::internalFetchAlbum(const QString &browseId)
{
try {
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
//
@@ -107,32 +81,21 @@ void AsyncYTMusic::internalFetchAlbum(const QString &browseId)
//
void 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()));
}
invokeAndCatchOnThread([=, this]() {
auto maybeSong = m_ytm->get_song(videoId.toStdString());
if (maybeSong.has_value()) {
Q_EMIT fetchSongFinished(*maybeSong);
}
});
}
//
// fetchPlaylist
//
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
Q_EMIT startFetchPlaylist(playlistId);
}
void AsyncYTMusic::internalFetchPlaylist(const QString &playlistId)
{
try {
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
//
@@ -140,16 +103,9 @@ void AsyncYTMusic::internalFetchPlaylist(const QString &playlistId)
//
void AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &params)
{
Q_EMIT startFetchArtistAlbums(channelId, params);
}
void AsyncYTMusic::internalFetchArtistAlbums(const QString &channelid, const QString &params)
{
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()));
}
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString()));
});
}
//
@@ -157,16 +113,9 @@ void AsyncYTMusic::internalFetchArtistAlbums(const QString &channelid, const QSt
//
void AsyncYTMusic::extractVideoInfo(const QString &videoId)
{
Q_EMIT startExtractVideoInfo(videoId);
}
void AsyncYTMusic::internalExtractVideoInfo(const QString &videoId)
{
try {
invokeAndCatchOnThread([=, this]() {
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
//
@@ -174,19 +123,22 @@ void AsyncYTMusic::internalExtractVideoInfo(const QString &videoId)
//
void AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
{
Q_EMIT startFetchWatchPlaylist(videoId, playlistId);
invokeAndCatchOnThread([=, this]() {
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
map_optional<std::string>(videoId, &QString::toStdString),
map_optional<std::string>(playlistId, &QString::toStdString)
));
});
}
void AsyncYTMusic::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()));
}
void AsyncYTMusic::invokeAndCatchOnThread(const std::function<void()> &fun) {
QMetaObject::invokeMethod(this, [=, this]() {
try {
fun();
} catch (const py::error_already_set &err) {
Q_EMIT errorOccurred(QString::fromLocal8Bit(err.what()));
}
});
}
YTMusicThread &YTMusicThread::instance()
+2 -23
View File
@@ -84,29 +84,8 @@ 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 &params);
Q_SLOT void internalFetchArtistAlbums(const QString &channelid, const QString &params);
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.
void invokeAndCatchOnThread(const std::function<void()> &fun);
// Python interpreter will be initialized from the thread calling the methods
Lazy<YTMusic> m_ytm;
+13 -1
View File
@@ -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
}
}
+26
View File
@@ -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()
}
]
}
+1
View File
@@ -223,6 +223,7 @@ Item {
Behavior on value {
NumberAnimation {
duration: 1000
}
}
+36 -3
View File
@@ -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
}
}
-8
View File
@@ -71,14 +71,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
+1 -1
View File
@@ -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;
+49 -52
View File
@@ -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;
};
+2 -2
View File
@@ -13,11 +13,11 @@
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) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [this](const playlist::Playlist &playlist) {
setLoading(false);
beginResetModel();
m_playlist = playlist;
+5 -4
View File
@@ -15,11 +15,12 @@ namespace PlaylistUtils {
QString artistsToString(const std::vector<meta::Artist> &artists)
{
if (!artists.empty()) {
return std::accumulate(artists.begin() + 1, artists.end(),
QString::fromStdString(artists.front().name),
[](QString &string, const meta::Artist &artist) {
return string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
QString string = QString::fromStdString(artists.front().name);
std::for_each(artists.begin() + 1, artists.end(),
[&string](const meta::Artist &artist) {
string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
});
return string;
}
return {};
+1
View File
@@ -12,5 +12,6 @@ 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>
</qresource>
</RCC>
+28 -3
View File
@@ -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();
@@ -18,13 +18,17 @@ SearchModel::SearchModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->search(m_searchQuery);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [=](const std::vector<search::SearchResultItem> &results) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [this](const std::vector<search::SearchResultItem> &results) {
beginResetModel();
setLoading(false);
m_searchResults = results;
std::sort(m_searchResults.begin(), m_searchResults.end(),
[](search::SearchResultItem const & a, search::SearchResultItem const & b) {
return SearchModel::itemType(a) < SearchModel::itemType(b);
});
endResetModel();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
setLoading(false);
});
}
@@ -165,3 +169,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);
}
+1
View File
@@ -55,4 +55,5 @@ public:
private:
QString m_searchQuery;
std::vector<search::SearchResultItem> m_searchResults;
static int itemType(search::SearchResultItem const &item);
};
+9 -9
View File
@@ -15,7 +15,7 @@
UserPlaylistModel::UserPlaylistModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=] {
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [this] {
if (m_initialVideoId.isEmpty()) {
return;
}
@@ -23,7 +23,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
});
connect(this, &UserPlaylistModel::playlistIdChanged, this, [=] {
connect(this, &UserPlaylistModel::playlistIdChanged, this, [this] {
if (m_playlistId.isEmpty()) {
return;
}
@@ -31,7 +31,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [=](const watch::Playlist &playlist) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [this](const watch::Playlist &playlist) {
setLoading(false);
beginResetModel();
@@ -49,7 +49,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setCurrentVideoId({});
}
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
setLoading(false);
});
}
@@ -101,7 +101,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 +129,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 +157,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 +239,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 +257,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();
});
+9 -13
View File
@@ -9,7 +9,7 @@
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();
@@ -23,7 +23,7 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [=](const video_info::VideoInfo &info) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [this](const video_info::VideoInfo &info) {
m_videoInfo = info;
setLoading(false);
Q_EMIT audioUrlChanged();
@@ -36,7 +36,7 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
QUrl VideoInfoExtractor::audioUrl() const
{
if (m_videoInfo.formats.empty()) {
return QUrl();
return {};
}
std::vector<video_info::Format> audioFormats;
@@ -48,17 +48,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));
}
@@ -81,7 +77,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
View File
@@ -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
View File
@@ -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;