Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69901cb69c | ||
|
|
e230adf223 | ||
|
|
07584b73f4 | ||
|
|
1b29a028a6 | ||
|
|
736d1789c9 | ||
|
|
01f22c517a | ||
|
|
edd171c97b | ||
|
|
873c791262 | ||
|
|
068a5b756a | ||
|
|
db35f2a5e6 | ||
|
|
c06bb7ad03 | ||
|
|
1c63395497 | ||
|
|
ba79cd6b5a |
No files matched your search
@@ -1,6 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
|
||||
# SPDX-FileCopyrightText: 2021 Bhushan Shah <[email protected]>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
*.kdev4
|
||||
CMakeLists.txt.user
|
||||
flatpak/flatpak-builder-tools/
|
||||
@@ -8,6 +8,7 @@ project(audiotube)
|
||||
|
||||
include(FeatureSummary)
|
||||
|
||||
set(PROJECT_VERSION "21.08")
|
||||
set(QT5_MIN_VERSION 5.15)
|
||||
set(KF5_MIN_VERSION 5.81)
|
||||
|
||||
@@ -25,6 +26,11 @@ include(ECMSetupVersion)
|
||||
include(ECMGenerateHeaders)
|
||||
include(ECMPoQmTools)
|
||||
|
||||
ecm_setup_version(${PROJECT_VERSION}
|
||||
VARIABLE_PREFIX AUDIOTUBE
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
)
|
||||
|
||||
kde_enable_exceptions()
|
||||
|
||||
find_package(Qt5 ${QT5_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg)
|
||||
|
||||
@@ -4,7 +4,7 @@ AudioTube is a client for YouTube Music.
|
||||
|
||||
# Dependencies
|
||||
|
||||
It depends on Qt and Kirigami in addition to a few python packages:
|
||||
It depends on Qt, Kirigami, KI18n, KCoreAddons and KCrash in addition to a few python packages:
|
||||
* ytmusicapi
|
||||
* youtube_dl
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[Project]
|
||||
CreatedFrom=CMakeLists.txt
|
||||
Manager=KDevCMakeManager
|
||||
Name=audiotube
|
||||
@@ -1,3 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
@@ -32,8 +32,8 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/1b/8e/2bc4c8160083bbfaac215637f1e9f4a43263833c2e81212cba3073aeaa25/ytmusicapi-0.17.3.tar.gz",
|
||||
"sha256": "9a249cc53efd6408034f401a9a4375272a9b339e8eb64f3a2e713a1ccd06d55b"
|
||||
"url": "https://files.pythonhosted.org/packages/83/aa/3e2c231fbef90d5bd75614e3739bb27e7e33e28c9891d6ee0561fe52a9df/ytmusicapi-0.18.0.tar.gz",
|
||||
"sha256": "447d1b6a594f49942f7ac75412cba59c191b6f3540d98ac65a1333445beec16d"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,7 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<summary xml:lang="it">Client YouTube Music convergente</summary>
|
||||
<summary xml:lang="ko">올인원 YouTube Music 클라이언트</summary>
|
||||
<summary xml:lang="nl">Convergente muziekclient van YouTube</summary>
|
||||
<summary xml:lang="pl">Całościowy program do muzyki z YouTube</summary>
|
||||
<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="sl">Konvergentni odjemalec glasbe YouTube</summary>
|
||||
@@ -76,6 +76,11 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<developer_name xml:lang="x-test">xxKDE Communityxx</developer_name>
|
||||
<content_rating type="oars-1.0"/>
|
||||
<url type="homepage">https://apps.kde.org/en/audiotube</url>
|
||||
<releases>
|
||||
<release version="21.08" date="2021-08-31">
|
||||
<url>https://www.plasma-mobile.org/2021/08/31/plasma-mobile-gear-21-08/</url>
|
||||
</release>
|
||||
</releases>
|
||||
<launchable type="desktop-id">org.kde.audiotube.desktop</launchable>
|
||||
<screenshots>
|
||||
<screenshot>
|
||||
@@ -104,7 +109,7 @@ SPDX-License-Identifier: CC0-1.0
|
||||
<p xml:lang="it">AudioTube può eseguire ricerche su YouTube Music, elencare album e artisti, riprodurre scalette e album generati automaticamente e ti consente di creare la tua scaletta.</p>
|
||||
<p xml:lang="ko">AudioTube는 YouTube Music을 검색하고, 앨범과 아티스트를 표시하고, 자동 생성된 재생 목록, 앨범을 재생하고, 사용자 정의 재생 목록을 만들 수 있습니다.</p>
|
||||
<p xml:lang="nl">AudioTube kan in YouTube Music zoeken, een lijst maken van albums en artiesten, automatically gegenereerde afspeellijsten, albums afspelen en biedt u het samenstellen van uw eigen afspeellijst.</p>
|
||||
<p xml:lang="pl">AudioTube może wyszukać muzyki na YouTube, wypisać albumy i wykonawców, odtwarzać samoczynnie utworzone listy odtwarzania, albumy oraz umożliwia tworzenie własnych list odtwarzania.</p>
|
||||
<p xml:lang="pl">AudioTube może przeszukiwać serwis YouTube Music, wypisać albumy i wykonawców, odtwarzać samoczynnie utworzone listy odtwarzania, albumy oraz umożliwia tworzenie własnych list odtwarzania.</p>
|
||||
<p xml:lang="pt">O AudioTube consegue pesquisar no YouTube Music, enumerar os álbuns e artistas, reproduzir listas geradas automaticamente, álbuns e permite-lhe reproduzir as suas próprias listas de reprodução.</p>
|
||||
<p xml:lang="pt-BR">O AudioTube pode pesquisar no YouTube Music, listar álbuns e artistas, rodar listas de reprodução geradas automaticamente, álbuns inteiros, e criar a sua própria lista de reprodução.</p>
|
||||
<p xml:lang="sl">AudioTube lahko išče po YouTube Music, izdela sezname albumov in avtorjev, samodejno predvaja generirane sezname, albume in sestavlja vaše sezname predvajanja.</p>
|
||||
|
||||
@@ -17,6 +17,7 @@ Name[pa]=ਆਡੀਓ-ਟਿਊਬ
|
||||
Name[pl]=AudioTube
|
||||
Name[pt]=AudioTube
|
||||
Name[pt_BR]=AudioTube
|
||||
Name[ro]=AudioTube
|
||||
Name[sk]=AudioTube
|
||||
Name[sl]=AudioTube
|
||||
Name[sv]=AudioTube
|
||||
@@ -37,9 +38,10 @@ Comment[it]=Client YouTube Music
|
||||
Comment[ko]=YouTube Music 클라이언트
|
||||
Comment[nl]=Muziekclient van YouTube
|
||||
Comment[pa]=ਯੂਟਿਊਬ ਸੰਗੀਤ ਕਲਾਈਂਟ
|
||||
Comment[pl]=Program do muzyki z YouTube
|
||||
Comment[pl]=Program do YouTube Music
|
||||
Comment[pt]=Cliente do YouTube Music
|
||||
Comment[pt_BR]=Cliente YouTube Music
|
||||
Comment[ro]=Client de muzică pentru YouTube
|
||||
Comment[sk]=Hudobný klient YouTube
|
||||
Comment[sl]=Glasbeni odjemalec Youtube
|
||||
Comment[sv]=YouTube-musikprogram
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
ytmusicapi==0.17.3
|
||||
ytmusicapi==0.18.0
|
||||
youtube_dl
|
||||
@@ -24,6 +24,7 @@ add_executable(audiotube
|
||||
playlistutils.cpp
|
||||
errorhandler.cpp
|
||||
playerutils.cpp
|
||||
abstractytmusicmodel.cpp
|
||||
multiiterableview.h
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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
|
||||
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
|
||||
|
||||
AbstractYTMusicModel::AbstractYTMusicModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool AbstractYTMusicModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void AbstractYTMusicModel::setLoading(bool loading)
|
||||
{
|
||||
if (m_loading == loading)
|
||||
return;
|
||||
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
///
|
||||
/// Base class for all models that work with data from the AsyncYTMusic class.
|
||||
/// It provides a loading property, that is automatically set to false if a query fails.
|
||||
///
|
||||
class AbstractYTMusicModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
explicit AbstractYTMusicModel(QObject *parent = nullptr);
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
private:
|
||||
bool m_loading = false;
|
||||
};
|
||||
|
||||
+1
-13
@@ -7,7 +7,7 @@
|
||||
#include <QDebug>
|
||||
|
||||
AlbumModel::AlbumModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &AlbumModel::browseIdChanged, this, [=] {
|
||||
setLoading(true);
|
||||
@@ -81,18 +81,6 @@ QString AlbumModel::title() const
|
||||
return QString::fromStdString(m_album.title);
|
||||
}
|
||||
|
||||
bool AlbumModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void AlbumModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
|
||||
QUrl AlbumModel::thumbnailUrl() const
|
||||
{
|
||||
if (m_album.thumbnails.empty()) {
|
||||
|
||||
+2
-7
@@ -8,15 +8,15 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
class AlbumModel : public QAbstractListModel
|
||||
class AlbumModel : public AbstractYTMusicModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString browseId READ browseId WRITE setBrowseId NOTIFY browseIdChanged REQUIRED)
|
||||
|
||||
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
|
||||
Q_PROPERTY(QUrl thumbnailUrl READ thumbnailUrl NOTIFY thumbnailUrlChanged)
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QString playlistId READ playlistId NOTIFY playlistIdChanged)
|
||||
|
||||
public:
|
||||
@@ -39,10 +39,6 @@ public:
|
||||
QString title() const;
|
||||
Q_SIGNAL void titleChanged();
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
QUrl thumbnailUrl() const;
|
||||
Q_SIGNAL void thumbnailUrlChanged();
|
||||
|
||||
@@ -51,7 +47,6 @@ public:
|
||||
|
||||
private:
|
||||
QString m_browseId;
|
||||
bool m_loading = false;
|
||||
|
||||
album::Album m_album;
|
||||
};
|
||||
+1
-12
@@ -7,7 +7,7 @@
|
||||
#include <asyncytmusic.h>
|
||||
|
||||
ArtistModel::ArtistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &ArtistModel::channelIdChanged, this, [=] {
|
||||
if (m_channelId.isEmpty()) {
|
||||
@@ -151,17 +151,6 @@ QUrl ArtistModel::thumbnailUrl() const
|
||||
return QUrl(QString::fromStdString(m_artist.thumbnails.back().url));
|
||||
}
|
||||
|
||||
bool ArtistModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void ArtistModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
void ArtistModel::triggerItem(int row)
|
||||
{
|
||||
if (m_view) {
|
||||
|
||||
+5
-8
@@ -10,9 +10,12 @@
|
||||
#include <optional>
|
||||
|
||||
#include <ytmusic.h>
|
||||
#include "multiiterableview.h"
|
||||
|
||||
class ArtistModel : public QAbstractListModel
|
||||
#include "multiiterableview.h"
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
|
||||
class ArtistModel : public AbstractYTMusicModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -20,7 +23,6 @@ class ArtistModel : public QAbstractListModel
|
||||
|
||||
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
|
||||
Q_PROPERTY(QUrl thumbnailUrl READ thumbnailUrl NOTIFY thumbnailUrlChanged)
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
enum Type {
|
||||
@@ -54,10 +56,6 @@ public:
|
||||
QUrl thumbnailUrl() const;
|
||||
Q_SIGNAL void thumbnailUrlChanged();
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
Q_INVOKABLE void triggerItem(int row);
|
||||
|
||||
Q_SIGNAL void openAlbum(const QString &browseId);
|
||||
@@ -66,7 +64,6 @@ public:
|
||||
|
||||
private:
|
||||
QString m_channelId;
|
||||
bool m_loading = false;
|
||||
|
||||
artist::Artist m_artist;
|
||||
std::optional<MultiIterableView<
|
||||
|
||||
+207
-198
@@ -9,243 +9,252 @@ import org.kde.kirigami 2.12 as Kirigami
|
||||
import org.kde.ytmusic 1.0
|
||||
import QtMultimedia 5.12
|
||||
|
||||
ColumnLayout {
|
||||
id: footerLayout
|
||||
|
||||
Item {
|
||||
id: footerItem
|
||||
property bool maximized: false
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: maximized ? applicationWindow().height : player.preferredHeight
|
||||
height: maximized ? applicationWindow().height : 75
|
||||
|
||||
GridLayout {
|
||||
id: playerLayout
|
||||
Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
anchors.fill: footerLayout
|
||||
}
|
||||
|
||||
readonly property bool mobile: width > height
|
||||
ColumnLayout {
|
||||
id: footerLayout
|
||||
height: parent.height
|
||||
anchors.fill: parent
|
||||
|
||||
Layout.fillHeight: footerLayout.maximized
|
||||
Layout.fillWidth: true
|
||||
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
|
||||
visible: footerLayout.maximized
|
||||
GridLayout {
|
||||
id: playerLayout
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: info.thumbnail
|
||||
Layout.fillHeight: info.thumbnail
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
readonly property bool mobile: width > height
|
||||
|
||||
source: info.thumbnail
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: footerItem.maximized
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
|
||||
visible: footerItem.maximized
|
||||
|
||||
Controls.ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
ListView {
|
||||
Item {
|
||||
Layout.fillWidth: info.thumbnail
|
||||
Layout.fillHeight: info.thumbnail
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
id: playlistView
|
||||
|
||||
clip: true
|
||||
|
||||
Controls.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: UserPlaylistModel.loading || UserPlaylistModel.loading
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
if (count < 1) {
|
||||
footerLayout.maximized = false
|
||||
}
|
||||
}
|
||||
|
||||
model: UserPlaylistModel
|
||||
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
id: delegateItem
|
||||
required property string title
|
||||
required property string videoId
|
||||
required property string artists
|
||||
required property bool isCurrent
|
||||
|
||||
highlighted: isCurrent
|
||||
onClicked: UserPlaylistModel.skipTo(videoId)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
level: 2
|
||||
text: title
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
text: artists
|
||||
}
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Remove Track")
|
||||
icon.name: "list-remove"
|
||||
onTriggered: UserPlaylistModel.remove(delegateItem.videoId)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
header: Controls.ToolBar {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
text: i18n("Clear")
|
||||
icon.name: "edit-clear-all"
|
||||
onClicked: UserPlaylistModel.clear()
|
||||
}
|
||||
Controls.ToolButton {
|
||||
text: i18n("Shuffle")
|
||||
icon.name: "media-playlist-shuffle"
|
||||
onClicked: UserPlaylistModel.shufflePlaylist()
|
||||
}
|
||||
}
|
||||
}
|
||||
source: info.thumbnail
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: player
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: !footer.maximized
|
||||
|
||||
VideoInfoExtractor {
|
||||
id: info
|
||||
|
||||
onSongChanged: audio.play()
|
||||
videoId: UserPlaylistModel.currentVideoId
|
||||
}
|
||||
|
||||
Audio {
|
||||
id: audio
|
||||
|
||||
source: info.audioUrl
|
||||
onStatusChanged: {
|
||||
if (status === Audio.EndOfMedia) {
|
||||
console.log("Song ended");
|
||||
UserPlaylistModel.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
Layout.fillHeight: true
|
||||
enabled: info.audioUrl != ""
|
||||
text: audio.playbackState === Audio.PlayingState ? i18n("Pause") : i18n("Play")
|
||||
visible: !info.loading
|
||||
icon.name: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
|
||||
onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play()
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
Layout.fillHeight: true
|
||||
enabled: UserPlaylistModel.canSkip
|
||||
visible: !info.loading
|
||||
icon.name: "media-skip-forward"
|
||||
onClicked: UserPlaylistModel.next()
|
||||
}
|
||||
|
||||
Controls.BusyIndicator {
|
||||
Layout.fillHeight: true
|
||||
visible: info.loading
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 3
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
elide: Qt.ElideRight
|
||||
text: info.title ? info.title : i18n("No media playing")
|
||||
Layout.fillHeight: true
|
||||
|
||||
Controls.ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
id: playlistView
|
||||
|
||||
clip: true
|
||||
|
||||
Controls.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: UserPlaylistModel.loading || UserPlaylistModel.loading
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
if (count < 1) {
|
||||
footerItem.maximized = false
|
||||
}
|
||||
}
|
||||
|
||||
model: UserPlaylistModel
|
||||
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
id: delegateItem
|
||||
required property string title
|
||||
required property string videoId
|
||||
required property string artists
|
||||
required property bool isCurrent
|
||||
|
||||
highlighted: isCurrent
|
||||
onClicked: UserPlaylistModel.skipTo(videoId)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
level: 2
|
||||
text: title
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
text: artists
|
||||
}
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Remove Track")
|
||||
icon.name: "list-remove"
|
||||
onTriggered: UserPlaylistModel.remove(delegateItem.videoId)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
header: Controls.ToolBar {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
text: i18n("Clear")
|
||||
icon.name: "edit-clear-all"
|
||||
onClicked: UserPlaylistModel.clear()
|
||||
}
|
||||
Controls.ToolButton {
|
||||
text: i18n("Shuffle")
|
||||
icon.name: "media-playlist-shuffle"
|
||||
onClicked: UserPlaylistModel.shufflePlaylist()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: player
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: !footer.maximized
|
||||
|
||||
VideoInfoExtractor {
|
||||
id: info
|
||||
|
||||
onSongChanged: audio.play()
|
||||
videoId: UserPlaylistModel.currentVideoId
|
||||
}
|
||||
|
||||
RowLayout{
|
||||
Audio {
|
||||
id: audio
|
||||
|
||||
Controls.Label{
|
||||
visible: info.title
|
||||
text: PlayerUtils.formatTimestamp(audio.position)
|
||||
source: info.audioUrl
|
||||
onStatusChanged: {
|
||||
if (status === Audio.EndOfMedia) {
|
||||
console.log("Song ended");
|
||||
UserPlaylistModel.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
Layout.fillHeight: true
|
||||
enabled: info.audioUrl != ""
|
||||
text: audio.playbackState === Audio.PlayingState ? i18n("Pause") : i18n("Play")
|
||||
visible: !info.loading
|
||||
icon.name: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
|
||||
onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play()
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
Layout.fillHeight: true
|
||||
enabled: UserPlaylistModel.canSkip
|
||||
visible: !info.loading
|
||||
icon.name: "media-skip-forward"
|
||||
onClicked: UserPlaylistModel.next()
|
||||
}
|
||||
|
||||
Controls.BusyIndicator {
|
||||
Layout.fillHeight: true
|
||||
visible: info.loading
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 3
|
||||
Layout.fillWidth: true
|
||||
elide: Qt.ElideRight
|
||||
text: info.title ? info.title : i18n("No media playing")
|
||||
}
|
||||
|
||||
Controls.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
enabled: audio.seekable
|
||||
onMoved: {
|
||||
console.log("Value:", value);
|
||||
audio.seek(Math.floor(value));
|
||||
RowLayout{
|
||||
|
||||
Controls.Label{
|
||||
visible: info.title
|
||||
text: PlayerUtils.formatTimestamp(audio.position)
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
Controls.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
enabled: audio.seekable
|
||||
onMoved: {
|
||||
console.log("Value:", value);
|
||||
audio.seek(Math.floor(value));
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Controls.Label{
|
||||
visible: info.title
|
||||
text: PlayerUtils.formatTimestamp(audio.duration)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Controls.Label{
|
||||
visible: info.title
|
||||
text: PlayerUtils.formatTimestamp(audio.duration)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
text: i18n("Expand")
|
||||
enabled: playlistView.count > 0
|
||||
Layout.fillHeight: true
|
||||
icon.name: footerItem.maximized ? "arrow-down" : "arrow-up"
|
||||
onClicked: footerItem.maximized = !footerItem.maximized
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.preferredWidth: parent.height
|
||||
text: i18n("Expand")
|
||||
enabled: playlistView.count > 0
|
||||
Layout.fillHeight: true
|
||||
icon.name: footerLayout.maximized ? "arrow-down" : "arrow-up"
|
||||
onClicked: footerLayout.maximized = !footerLayout.maximized
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -118,7 +118,7 @@ Kirigami.ScrollablePage {
|
||||
text: i18n("Find music on YouTube Music")
|
||||
helpfulAction: Kirigami.Action {
|
||||
onTriggered: searchField.forceActiveFocus()
|
||||
text: "Search"
|
||||
text: i18n("Search")
|
||||
icon.name: "search"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ Kirigami.ApplicationWindow {
|
||||
property alias searchField: searchField
|
||||
|
||||
header: Controls.Control {
|
||||
visible: !footer.maximized
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
+1
-12
@@ -11,7 +11,7 @@
|
||||
#include "playlistutils.h"
|
||||
|
||||
PlaylistModel::PlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &PlaylistModel::playlistIdChanged, this, [=] {
|
||||
setLoading(true);
|
||||
@@ -76,17 +76,6 @@ void PlaylistModel::setPlaylistId(const QString &playlistId)
|
||||
Q_EMIT playlistIdChanged();
|
||||
}
|
||||
|
||||
bool PlaylistModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void PlaylistModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
QUrl PlaylistModel::thumbnailUrl() const
|
||||
{
|
||||
if (m_playlist.thumbnails.empty()) {
|
||||
|
||||
+3
-7
@@ -7,7 +7,9 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <ytmusic.h>
|
||||
|
||||
class PlaylistModel : public QAbstractListModel
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
class PlaylistModel : public AbstractYTMusicModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -15,7 +17,6 @@ class PlaylistModel : public QAbstractListModel
|
||||
Q_PROPERTY(QString playlistId READ playlistId WRITE setPlaylistId NOTIFY playlistIdChanged REQUIRED)
|
||||
|
||||
// output
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QUrl thumbnailUrl READ thumbnailUrl NOTIFY thumbnailUrlChanged)
|
||||
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
|
||||
|
||||
@@ -36,10 +37,6 @@ public:
|
||||
void setPlaylistId(const QString &playlistId);
|
||||
Q_SIGNAL void playlistIdChanged();
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
QUrl thumbnailUrl() const;
|
||||
Q_SIGNAL void thumbnailUrlChanged();
|
||||
|
||||
@@ -48,7 +45,6 @@ public:
|
||||
|
||||
private:
|
||||
QString m_playlistId;
|
||||
bool m_loading = false;
|
||||
|
||||
playlist::Playlist m_playlist {};
|
||||
};
|
||||
+1
-12
@@ -5,7 +5,7 @@
|
||||
#include "searchmodel.h"
|
||||
|
||||
SearchModel::SearchModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &SearchModel::searchQueryChanged, this, [=] {
|
||||
if (m_searchQuery.isEmpty()) {
|
||||
@@ -146,17 +146,6 @@ void SearchModel::setSearchQuery(const QString &searchQuery)
|
||||
Q_EMIT searchQueryChanged();
|
||||
}
|
||||
|
||||
bool SearchModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void SearchModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
void SearchModel::triggerItem(int row)
|
||||
{
|
||||
std::visit([&](auto&& arg) {
|
||||
|
||||
+2
-7
@@ -8,13 +8,13 @@
|
||||
#include <QThread>
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
class SearchModel : public QAbstractListModel
|
||||
class SearchModel : public AbstractYTMusicModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString searchQuery READ searchQuery WRITE setSearchQuery NOTIFY searchQueryChanged)
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
enum Type {
|
||||
@@ -44,10 +44,6 @@ public:
|
||||
void setSearchQuery(const QString &searchQuery);
|
||||
Q_SIGNAL void searchQueryChanged();
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
Q_INVOKABLE void triggerItem(int row);
|
||||
|
||||
Q_SIGNAL void openAlbum(const QString &browseId);
|
||||
@@ -59,5 +55,4 @@ public:
|
||||
private:
|
||||
QString m_searchQuery;
|
||||
std::vector<search::SearchResultItem> m_searchResults;
|
||||
bool m_loading = false;
|
||||
};
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "playlistutils.h"
|
||||
|
||||
UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractYTMusicModel(parent)
|
||||
{
|
||||
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=] {
|
||||
if (m_initialVideoId.isEmpty()) {
|
||||
@@ -98,17 +98,6 @@ void UserPlaylistModel::setInitialVideoId(const QString &videoId)
|
||||
Q_EMIT initialVideoIdChanged();
|
||||
}
|
||||
|
||||
bool UserPlaylistModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void UserPlaylistModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
QString UserPlaylistModel::nextVideoId() const
|
||||
{
|
||||
auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
#include <ytmusic.h>
|
||||
|
||||
class UserPlaylistModel : public QAbstractListModel
|
||||
#include "abstractytmusicmodel.h"
|
||||
|
||||
class UserPlaylistModel : public AbstractYTMusicModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -17,7 +19,6 @@ class UserPlaylistModel : public QAbstractListModel
|
||||
Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged)
|
||||
|
||||
// output
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QString currentVideoId READ currentVideoId NOTIFY currentVideoIdChanged)
|
||||
Q_PROPERTY(bool canSkip READ canSkip NOTIFY canSkipChanged)
|
||||
|
||||
@@ -39,10 +40,6 @@ public:
|
||||
void setInitialVideoId(const QString &videoId);
|
||||
Q_SIGNAL void initialVideoIdChanged();
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
Q_SIGNAL void loadingChanged();
|
||||
|
||||
QString nextVideoId() const;
|
||||
|
||||
QString currentVideoId() const;
|
||||
@@ -74,7 +71,6 @@ private:
|
||||
QString m_initialVideoId;
|
||||
QString m_playlistId;
|
||||
QString m_currentVideoId;
|
||||
bool m_loading = false;
|
||||
bool m_shuffle = false;
|
||||
|
||||
watch::Playlist m_playlist;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ using namespace py::literals;
|
||||
|
||||
#define UNEXPORT __attribute__ ((visibility("hidden")))
|
||||
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.17.3";
|
||||
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.18.0";
|
||||
|
||||
struct UNEXPORT YTMusicPrivate {
|
||||
py::scoped_interpreter guard {};
|
||||
|
||||
Reference in new issue
Block a user