Compare commits

..
22 changed files with 468 additions and 39 deletions

No files matched your search

+3 -1
View File
@@ -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})
+1 -1
View File
@@ -120,7 +120,7 @@ struct AsyncSqlDatabasePrivate {
// Internal asynchronous database class
QFuture<void> AsyncSqlDatabase::establishConnection(const DatabaseConfiguration &configuration)
{
return runAsync<void>([=, this] {
return runAsync([=, this] {
d->database = QSqlDatabase::addDatabase(configuration.type());
if (configuration.databaseName()) {
d->database.setDatabaseName(*configuration.databaseName());
+9 -8
View File
@@ -98,7 +98,7 @@ public:
template <typename T, typename ...Args>
auto getResults(const QString &sqlQuery, Args... args) -> QFuture<std::vector<T>> {
return runAsync<std::vector<T>>([=, this] {
return runAsync([=, this] {
auto query = executeQuery(sqlQuery, args...);
const auto rows = parseRows<typename T::ColumnTypes>(retrieveRows(query));
@@ -110,7 +110,7 @@ public:
template <typename T, typename ...Args>
auto getResult(const QString &sqlQuery, Args... args) -> QFuture<std::optional<T>> {
return runAsync<std::optional<T>>([=, this]() -> std::optional<T> {
return runAsync([=, this]() -> std::optional<T> {
auto query = executeQuery(sqlQuery, args...);
if (const auto row = retrieveOptionalRow(query)) {
return T::fromSql(parseRow<typename T::ColumnTypes>(*row));
@@ -122,13 +122,13 @@ public:
template <typename ...Args>
auto execute(const QString &sqlQuery, Args... args) -> QFuture<void> {
return runAsync<void>([=, this] {
return runAsync([=, this] {
executeQuery(sqlQuery, args...);
});
}
auto runMigrations(const QString &migrationDirectory) -> QFuture<void> {
return runAsync<void>([=, this] {
return runAsync([=, this] {
runDatabaseMigrations(db(), migrationDirectory);
});
}
@@ -149,11 +149,12 @@ private:
return runQuery(query);
}
template <typename T, typename Functor>
QFuture<T> runAsync(Functor func) {
auto interface = std::make_shared<QFutureInterface<T>>();
template <typename Functor>
QFuture<std::invoke_result_t<Functor>> runAsync(Functor func) {
using ReturnType = std::invoke_result_t<Functor>;
auto interface = std::make_shared<QFutureInterface<ReturnType>>();
QMetaObject::invokeMethod(this, [interface, func] {
if constexpr (!std::is_same_v<T, void>) {
if constexpr (!std::is_same_v<ReturnType, void>) {
auto result = func();
interface->reportResult(result);
} else {
+12 -12
View File
@@ -23,8 +23,8 @@ namespace py = pybind11;
#include <type_traits>
#include <memory>
template <typename R, typename T, typename OP>
std::optional<R> map_optional(const std::optional<T> &optional, OP op) {
template <typename T, typename OP>
std::optional<std::invoke_result_t<OP, T>> mapOptional(const std::optional<T> &optional, OP op) {
if (optional.has_value()) {
if constexpr (std::is_member_function_pointer<OP>::value) {
return (&optional.value()->*op)();
@@ -61,7 +61,7 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
//
QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QString &query)
{
return invokeAndCatchOnThread<std::vector<search::SearchResultItem>>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->search(query.toStdString());
});
}
@@ -71,7 +71,7 @@ QFuture<std::vector<search::SearchResultItem>> AsyncYTMusic::search(const QStrin
//
QFuture<artist::Artist> AsyncYTMusic::fetchArtist(const QString &channelId)
{
return invokeAndCatchOnThread<artist::Artist>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_artist(channelId.toStdString());
});
}
@@ -81,7 +81,7 @@ QFuture<artist::Artist> AsyncYTMusic::fetchArtist(const QString &channelId)
//
QFuture<album::Album> AsyncYTMusic::fetchAlbum(const QString &browseId)
{
return invokeAndCatchOnThread<album::Album>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_album(browseId.toStdString());
});
}
@@ -91,7 +91,7 @@ QFuture<album::Album> AsyncYTMusic::fetchAlbum(const QString &browseId)
//
QFuture<std::optional<song::Song>> AsyncYTMusic::fetchSong(const QString &videoId)
{
return invokeAndCatchOnThread<std::optional<song::Song>>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_song(videoId.toStdString());
});
}
@@ -100,7 +100,7 @@ QFuture<std::optional<song::Song>> AsyncYTMusic::fetchSong(const QString &videoI
// fetchPlaylist
//
QFuture<playlist::Playlist> AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
return invokeAndCatchOnThread<playlist::Playlist>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_playlist(playlistId.toStdString());
});
}
@@ -110,7 +110,7 @@ QFuture<playlist::Playlist> AsyncYTMusic::fetchPlaylist(const QString &playlistI
//
QFuture<std::vector<artist::Artist::Album>> AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &params)
{
return invokeAndCatchOnThread<std::vector<artist::Artist::Album>>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString());
});
}
@@ -120,7 +120,7 @@ QFuture<std::vector<artist::Artist::Album>> AsyncYTMusic::fetchArtistAlbums(cons
//
QFuture<video_info::VideoInfo> AsyncYTMusic::extractVideoInfo(const QString &videoId)
{
return invokeAndCatchOnThread<video_info::VideoInfo>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->extract_video_info(videoId.toStdString());
});
}
@@ -130,10 +130,10 @@ QFuture<video_info::VideoInfo> AsyncYTMusic::extractVideoInfo(const QString &vid
//
QFuture<watch::Playlist> AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
{
return invokeAndCatchOnThread<watch::Playlist>([=, this]() {
return invokeAndCatchOnThread([=, this]() {
return m_ytm->get_watch_playlist(
map_optional<std::string>(videoId, &QString::toStdString),
map_optional<std::string>(playlistId, &QString::toStdString)
mapOptional(videoId, &QString::toStdString),
mapOptional(playlistId, &QString::toStdString)
);
});
}
+7 -5
View File
@@ -75,12 +75,13 @@ void connectFuture(const QFuture<T> &future, QObjectDerivedType *self, const Fun
/// Applies a function to a the contained value of a QFuture once it finishes.
/// @returns a QFuture that finishes once the future given as first argument finished
///
template <typename T, typename U, typename Func>
QFuture<U> mapFuture(const QFuture<T> &future, Func mapFunction) {
template <typename T, typename Func>
QFuture<std::invoke_result_t<Func, T>> mapFuture(const QFuture<T> &future, Func mapFunction) {
using ReturnType = std::invoke_result_t<Func, T>;
auto watcher = std::make_shared<QFutureWatcher<T>>();
watcher->setFuture(future);
auto interface = std::make_shared<QFutureInterface<U>>();
auto interface = std::make_shared<QFutureInterface<ReturnType>>();
QObject::connect(watcher.get(), &QFutureWatcherBase::finished, watcher.get(), [interface, watcher, mapFunction] {
auto result = watcher->result();
auto mapped = mapFunction(std::move(result));
@@ -123,8 +124,9 @@ protected:
private:
/// Invokes the given function on the thread of the YTMusic object, and handles exceptions that occur while invoking it.
template <typename ReturnType>
QFuture<ReturnType> invokeAndCatchOnThread(const std::function<ReturnType()> &fun) {
template <typename Func>
QFuture<std::invoke_result_t<Func>> invokeAndCatchOnThread(Func fun) {
using ReturnType = std::invoke_result_t<Func>;
auto interface = std::make_shared<QFutureInterface<ReturnType>>();
QMetaObject::invokeMethod(this, [=, this]() {
try {
+93 -1
View File
@@ -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
}
}
}
}
}
}
}
+27
View File
@@ -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
}
}
}
+42
View File
@@ -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
+10 -2
View File
@@ -1,3 +1,7 @@
// 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
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as Controls
@@ -30,6 +34,10 @@ Kirigami.ScrollablePage {
required property string videoId
required property string artists
onClicked: {
play(videoId)
}
RowLayout {
ThumbnailSource {
id: thumbnailSource
@@ -63,12 +71,12 @@ Kirigami.ScrollablePage {
Kirigami.Action {
icon.name: "go-next"
text: i18n("Play Next")
onTriggered: UserPlaylistModel.playNext(delegateItem.videoId, delegateItem.title, delegateItem.artists)
onTriggered: UserPlaylistModel.playNext(delegateItem.videoId, delegateItem.title, [])
},
Kirigami.Action {
icon.name: "media-playlist-append"
text: i18n("Add to Playlist")
onTriggered: UserPlaylistModel.append(delegateItem.videoId, delegateItem.title, delegateItem.artists)
onTriggered: UserPlaylistModel.append(delegateItem.videoId, delegateItem.title, [])
}
]
}
+1 -1
View File
@@ -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;
+14
View File
@@ -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");
+5
View File
@@ -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();
+67
View File
@@ -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();
});
}
+49
View File
@@ -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;
};
+55
View File
@@ -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();
});
}
+47
View File
@@ -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;
};
+4
View File
@@ -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
View File
@@ -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>
+2 -1
View File
@@ -353,12 +353,13 @@ 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>>(),
artist["name"].cast<std::string>(),
artist["channelId"].cast<std::string>(),
artist["subscribers"].cast<std::string>(),
artist["subscribers"].cast<std::optional<std::string>>(),
artist["subscribed"].cast<bool>(),
extract_py_list<meta::Thumbnail>(artist["thumbnails"]),
extract_artist_section<artist::Artist::Song>(artist, "songs"),
+1 -1
View File
@@ -131,7 +131,7 @@ struct Artist {
std::optional<std::string> views;
std::string name;
std::string channel_id;
std::string subscribers;
std::optional<std::string> subscribers;
bool subscribed;
std::vector<meta::Thumbnail> thumbnails;
std::optional<Section<Song>> songs;