Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1337323a7c | ||
|
|
a5a0caa328 | ||
|
|
96516e1590 |
No files matched your search
+3
-1
@@ -35,6 +35,8 @@ add_executable(audiotube
|
||||
abstractytmusicmodel.cpp
|
||||
multiiterableview.h
|
||||
library.cpp
|
||||
localplaylistmodel.cpp
|
||||
localplaylistsmodel.cpp
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
@@ -52,6 +54,6 @@ target_link_libraries(audiotube
|
||||
asyncdatabase
|
||||
)
|
||||
|
||||
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING -DQT_QML_DEBUG)
|
||||
target_compile_definitions(audiotube PRIVATE -DQT_NO_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING)
|
||||
|
||||
install(TARGETS audiotube ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
@@ -5,7 +5,7 @@
|
||||
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.kirigami 2.19 as Kirigami
|
||||
|
||||
import org.kde.ytmusic 1.0
|
||||
|
||||
@@ -46,6 +46,7 @@ Kirigami.ScrollablePage {
|
||||
Controls.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Library.favourites
|
||||
@@ -117,6 +118,8 @@ Kirigami.ScrollablePage {
|
||||
Controls.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Library.mostPlayed
|
||||
@@ -155,5 +158,94 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
text: i18n("Playlists")
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
text: i18n("Add")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
icon.name: "list-add"
|
||||
onClicked: addPlaylistDialog.open()
|
||||
}
|
||||
Controls.ToolButton {
|
||||
text: i18n("Show All")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
icon.name: "arrow-right"
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: addPlaylistDialog
|
||||
title: i18n("Add playlist")
|
||||
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||
|
||||
mainItem: Kirigami.FormLayout {
|
||||
Controls.TextField {
|
||||
id: titleField
|
||||
|
||||
Kirigami.FormData.label: i18n("Playlist Title")
|
||||
}
|
||||
Controls.TextField {
|
||||
id: descriptionField
|
||||
|
||||
Kirigami.FormData.label: i18n("Playlist Description")
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: Library.addPlaylist(titleField.text, descriptionField.text)
|
||||
}
|
||||
|
||||
Controls.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: LocalPlaylistsModel {
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: playlistDelegate
|
||||
required property string playlistId
|
||||
required property string title
|
||||
required property string description
|
||||
required property date createdOn
|
||||
|
||||
Layout.fillWidth: false
|
||||
Layout.maximumWidth: 200
|
||||
Kirigami.Card {
|
||||
Layout.preferredHeight: 200
|
||||
|
||||
header: Image {
|
||||
sourceSize: "200x200"
|
||||
source: ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
onClicked: pageStack.layers.push("qrc:/LocalPlaylistPage.qml", {
|
||||
"playlistId": playlistDelegate.playlistId,
|
||||
"title": playlistDelegate.title
|
||||
})
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.maximumWidth: 200
|
||||
elide: Qt.ElideRight
|
||||
text: playlistDelegate.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick 2.15
|
||||
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
import org.kde.ytmusic 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
property alias playlistId: playlistModel.playlistId
|
||||
|
||||
ListView {
|
||||
Kirigami.PlaceholderMessage {
|
||||
text: i18n("This playlist is still empty")
|
||||
anchors.centerIn: parent
|
||||
visible: parent.count < 1
|
||||
}
|
||||
|
||||
model: LocalPlaylistModel {
|
||||
id: playlistModel
|
||||
}
|
||||
|
||||
delegate: Kirigami.BasicListItem {
|
||||
required property string title
|
||||
|
||||
text: title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,6 +347,48 @@ Item {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: playlistsDialog
|
||||
|
||||
property string videoId
|
||||
|
||||
mainItem: ListView {
|
||||
implicitHeight: 200
|
||||
model: LocalPlaylistsModel {
|
||||
}
|
||||
|
||||
delegate: Kirigami.BasicListItem {
|
||||
required property string title
|
||||
required property int playlistId
|
||||
|
||||
text: title
|
||||
|
||||
onClicked: Library.addPlaylistEntry(playlistId, playlistsDialog.videoId, info.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: addToPlaylistButton
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
|
||||
Layout.maximumWidth: height
|
||||
Layout.preferredWidth: height
|
||||
|
||||
onClicked: {
|
||||
playlistsDialog.videoId = info.videoId
|
||||
playlistsDialog.open()
|
||||
}
|
||||
|
||||
icon.name: "media-playlist-append"
|
||||
icon.width: Kirigami.Units.gridUnit * 1.5
|
||||
icon.height: Kirigami.Units.gridUnit * 1.5
|
||||
icon.color: "white"
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: skipBackwardButton
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
int main() {
|
||||
const auto ytm = YTMusic();
|
||||
const auto results = ytm.search("Another one bites the dust");
|
||||
const auto results = ytm.search("Leoniden");
|
||||
|
||||
std::cout << "Found " << results.size() << " results." << std::endl;
|
||||
|
||||
|
||||
@@ -89,6 +89,20 @@ void Library::addPlaybackHistoryItem(const QString &videoId, const QString &titl
|
||||
});
|
||||
}
|
||||
|
||||
void Library::addPlaylist(const QString &title, const QString &description)
|
||||
{
|
||||
connectFuture(m_database->execute("insert into playlists (title, description) values (?, ?)", title, description), this, &Library::playlistsChanged);
|
||||
}
|
||||
|
||||
void Library::addPlaylistEntry(qint64 playlistId, const QString &videoId, const QString &title)
|
||||
{
|
||||
connectFuture(addSong(videoId, title), this, [=, this] {
|
||||
connectFuture(m_database->execute("insert into playlist_entries (playlist_id, video_id) values (?, ?)", playlistId, videoId), this, [=, this] {
|
||||
Q_EMIT playlistEntriesChanged(playlistId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
PlaybackHistoryModel *Library::mostPlayed()
|
||||
{
|
||||
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
|
||||
|
||||
@@ -126,6 +126,11 @@ public:
|
||||
Q_SIGNAL void playbackHistoryChanged();
|
||||
Q_INVOKABLE void addPlaybackHistoryItem(const QString &videoId, const QString &title);
|
||||
|
||||
Q_SIGNAL void playlistsChanged();
|
||||
Q_INVOKABLE void addPlaylist(const QString &title, const QString &description);
|
||||
Q_INVOKABLE void addPlaylistEntry(qint64 playlistId, const QString &videoId, const QString &title);
|
||||
Q_SIGNAL void playlistEntriesChanged(quint64 playlistId);
|
||||
|
||||
PlaybackHistoryModel *mostPlayed();
|
||||
|
||||
QNetworkAccessManager &nam();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: 2022 Jonah Brüchert <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "localplaylistmodel.h"
|
||||
|
||||
#include "library.h"
|
||||
|
||||
LocalPlaylistModel::LocalPlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(this, &LocalPlaylistModel::playlistIdChanged,
|
||||
this, &LocalPlaylistModel::refreshModel);
|
||||
connect(&Library::instance(), &Library::playlistEntriesChanged,
|
||||
this, &LocalPlaylistModel::refreshModel);
|
||||
}
|
||||
|
||||
int LocalPlaylistModel::rowCount(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid() ? 0 : m_entries.size();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LocalPlaylistModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ Roles::VideoId, "videoId" },
|
||||
{ Roles::Title, "title" }
|
||||
};
|
||||
}
|
||||
|
||||
QVariant LocalPlaylistModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case Roles::Title:
|
||||
return m_entries[index.row()].title;
|
||||
case Roles::VideoId:
|
||||
return m_entries[index.row()].videoId;
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
QString LocalPlaylistModel::playlistId() const
|
||||
{
|
||||
return m_playlistId;
|
||||
}
|
||||
|
||||
void LocalPlaylistModel::setPlaylistId(const QString &playlistId)
|
||||
{
|
||||
m_playlistId = playlistId;
|
||||
Q_EMIT playlistIdChanged();
|
||||
}
|
||||
|
||||
void LocalPlaylistModel::refreshModel()
|
||||
{
|
||||
auto future = Library::instance()
|
||||
.database()
|
||||
.getResults<PlaylistEntry>(
|
||||
"select video_id, title from "
|
||||
"playlist_entries natural join songs where playlist_id = ?", m_playlistId);
|
||||
|
||||
connectFuture(future, this, [this](auto entries) {
|
||||
beginResetModel();
|
||||
m_entries = entries;
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2022 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>
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct PlaylistEntry {
|
||||
using ColumnTypes = std::tuple<QString, QString>;
|
||||
|
||||
static PlaylistEntry fromSql(ColumnTypes tuple) {
|
||||
auto [videoId, title] = tuple;
|
||||
return PlaylistEntry { videoId, title };
|
||||
}
|
||||
|
||||
QString videoId;
|
||||
QString title;
|
||||
};
|
||||
|
||||
class LocalPlaylistModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString playlistId READ playlistId WRITE setPlaylistId NOTIFY playlistIdChanged)
|
||||
|
||||
enum Roles {
|
||||
VideoId = Qt::UserRole + 1,
|
||||
Title
|
||||
};
|
||||
|
||||
public:
|
||||
LocalPlaylistModel(QObject *parent = nullptr);
|
||||
int rowCount(const QModelIndex &index) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
QString playlistId() const;
|
||||
void setPlaylistId(const QString &playlistId);
|
||||
Q_SIGNAL void playlistIdChanged();
|
||||
|
||||
void refreshModel();
|
||||
|
||||
private:
|
||||
QString m_playlistId;
|
||||
std::vector<PlaylistEntry> m_entries;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2022 Jonah Brüchert <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "localplaylistsmodel.h"
|
||||
|
||||
#include "library.h"
|
||||
|
||||
LocalPlaylistsModel::LocalPlaylistsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(&Library::instance(), &Library::playlistsChanged,
|
||||
this, &LocalPlaylistsModel::refreshModel);
|
||||
refreshModel();
|
||||
}
|
||||
|
||||
int LocalPlaylistsModel::rowCount(const QModelIndex &index) const
|
||||
{
|
||||
return index.isValid() ? 0 : m_playlists.size();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LocalPlaylistsModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{Roles::PlaylistId, "playlistId"},
|
||||
{Roles::Title, "title"},
|
||||
{Roles::Description, "description"},
|
||||
{Roles::CreatedOn, "createdOn"}
|
||||
};
|
||||
}
|
||||
|
||||
QVariant LocalPlaylistsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case Roles::PlaylistId:
|
||||
return m_playlists[index.row()].playlistId;
|
||||
case Roles::Title:
|
||||
return m_playlists[index.row()].title;
|
||||
case Roles::Description:
|
||||
return m_playlists[index.row()].description;
|
||||
case Roles::CreatedOn:
|
||||
return m_playlists[index.row()].createdOn;
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
void LocalPlaylistsModel::refreshModel()
|
||||
{
|
||||
connectFuture(Library::instance().database().getResults<Playlist>("select * from playlists"), this, [this](auto playlists) {
|
||||
beginResetModel();
|
||||
m_playlists = playlists;
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2022 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>
|
||||
#include <QDateTime>
|
||||
|
||||
struct Playlist {
|
||||
using ColumnTypes = std::tuple<QString, QString, QString, QDateTime>;
|
||||
|
||||
Playlist static fromSql(ColumnTypes tuple) {
|
||||
auto [playlistId, title, description, createdOn] = tuple;
|
||||
return Playlist { playlistId, title, description, createdOn };
|
||||
}
|
||||
|
||||
QString playlistId;
|
||||
QString title;
|
||||
QString description;
|
||||
QDateTime createdOn;
|
||||
};
|
||||
|
||||
class LocalPlaylistsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
enum Roles {
|
||||
PlaylistId,
|
||||
Title,
|
||||
Description,
|
||||
CreatedOn
|
||||
};
|
||||
|
||||
public:
|
||||
LocalPlaylistsModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &index) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
void refreshModel();
|
||||
|
||||
private:
|
||||
std::vector<Playlist> m_playlists;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "playlistmodel.h"
|
||||
#include "playerutils.h"
|
||||
#include "library.h"
|
||||
#include "localplaylistmodel.h"
|
||||
#include "localplaylistsmodel.h"
|
||||
|
||||
#include "asyncdatabase.h"
|
||||
|
||||
@@ -45,6 +47,8 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
qmlRegisterType<VideoInfoExtractor>(URI, 1, 0, "VideoInfoExtractor");
|
||||
qmlRegisterType<ArtistModel>(URI, 1, 0, "ArtistModel");
|
||||
qmlRegisterType<PlaylistModel>(URI, 1, 0, "PlaylistModel");
|
||||
qmlRegisterType<LocalPlaylistModel>(URI, 1, 0, "LocalPlaylistModel");
|
||||
qmlRegisterType<LocalPlaylistsModel>(URI, 1, 0, "LocalPlaylistsModel");
|
||||
qmlRegisterSingletonType<UserPlaylistModel>(URI, 1, 0, "UserPlaylistModel", [](QQmlEngine *, QJSEngine *) {
|
||||
return new UserPlaylistModel();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
drop table playlists;
|
||||
drop table playlist_entries;
|
||||
@@ -0,0 +1,14 @@
|
||||
create table playlists (
|
||||
playlist_id Integer primary key autoincrement not null,
|
||||
title Text not null,
|
||||
description Text,
|
||||
created_on Timestamp not null default current_timestamp
|
||||
);
|
||||
|
||||
create table playlist_entries (
|
||||
playlist_id Integer not null,
|
||||
video_id Text not null,
|
||||
primary key (playlist_id, video_id)
|
||||
foreign key (video_id) references songs(video_id),
|
||||
foreign key (playlist_id) references playlists(playlist_id)
|
||||
);
|
||||
+3
-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>
|
||||
@@ -19,5 +13,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<file>migrations/2022-05-20-194850_init/down.sql</file>
|
||||
<file>migrations/2022-05-20-194850_init/up.sql</file>
|
||||
<file alias="PlaybackHistory.qml">contents/ui/PlaybackHistory.qml</file>
|
||||
<file>migrations/2022-05-25-212054_playlists/down.sql</file>
|
||||
<file>migrations/2022-05-25-212054_playlists/up.sql</file>
|
||||
<file alias="LocalPlaylistPage.qml">contents/ui/LocalPlaylistPage.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -353,6 +353,7 @@ std::vector<search::SearchResultItem> YTMusic::search(
|
||||
artist::Artist YTMusic::get_artist(const std::string &channel_id) const
|
||||
{
|
||||
const auto artist = d->get_ytmusic().attr("get_artist")(channel_id);
|
||||
pyPrintPretty(artist);
|
||||
return artist::Artist {
|
||||
optional_key<std::string>(artist, "description"),
|
||||
artist["views"].cast<std::optional<std::string>>(),
|
||||
|
||||
Reference in new issue
Block a user