Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc3710c200 | ||
|
|
48cf377194 | ||
|
|
07f5043016 | ||
|
|
2819ed9501 | ||
|
|
0c0dca262d | ||
|
|
b4a2331097 | ||
|
|
4ea2fa3a46 | ||
|
|
1d987c14cd | ||
|
|
35f38e8598 | ||
|
|
e015301863 | ||
|
|
94a8edeef3 |
No files matched your search
+5
-11
@@ -17,20 +17,14 @@ include(FeatureSummary)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(QT_MIN_VERSION "5.15")
|
||||
set(KF_MIN_VERSION "5.81.0")
|
||||
set(QT_MIN_VERSION "6.4.0")
|
||||
set(KF_MIN_VERSION "5.80.0")
|
||||
set(KF_MAJOR_VERSION "6")
|
||||
set(QT_MAJOR_VERSION "6")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
if (QT_MAJOR_VERSION STREQUAL "6")
|
||||
set(QT_MIN_VERSION "6.4.0")
|
||||
set(KF_MIN_VERSION "5.240.0")
|
||||
set(KF_MAJOR_VERSION "6")
|
||||
else()
|
||||
set(KF_MAJOR_VERSION "5")
|
||||
endif()
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
@@ -53,7 +47,7 @@ find_package(Ytdlp REQUIRED RUNTIME)
|
||||
find_package(YTMusicAPI 1.0.2 REQUIRED RUNTIME)
|
||||
find_package(KF6KirigamiAddons 0.6.0 REQUIRED)
|
||||
find_package(FutureSQL${QT_MAJOR_VERSION} REQUIRED)
|
||||
find_package(QCoro${QT_MAJOR_VERSION} 0.10.0 REQUIRED COMPONENTS Core)
|
||||
find_package(QCoro${QT_MAJOR_VERSION} 0.10.0 REQUIRED COMPONENTS Core Network Qml)
|
||||
|
||||
ki18n_install(po)
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ add_executable(audiotube
|
||||
resources.qrc
|
||||
blur.cpp
|
||||
clipboard.cpp
|
||||
downloadmanager.cpp
|
||||
downloadedsongsmodel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(audiotube
|
||||
@@ -54,6 +56,8 @@ target_link_libraries(audiotube
|
||||
FutureSQL${QT_MAJOR_VERSION}::FutureSQL
|
||||
QCoro${QT_MAJOR_VERSION}::Core
|
||||
qtmprisdeclarative
|
||||
QCoro${QT_MAJOR_VERSION}::Network
|
||||
QCoro${QT_MAJOR_VERSION}::Qml
|
||||
ytm
|
||||
)
|
||||
|
||||
|
||||
@@ -201,3 +201,29 @@ YTMusicThread::YTMusicThread()
|
||||
m_ytm->moveToThread(this);
|
||||
start();
|
||||
}
|
||||
|
||||
QUrl pickAudioUrl(const std::span<const video_info::Format> formats)
|
||||
{
|
||||
if (formats.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<video_info::Format> audioFormats;
|
||||
|
||||
// filter audio only formats
|
||||
std::copy_if(formats.begin(), formats.end(), std::back_inserter(audioFormats),
|
||||
[](const video_info::Format &format) {
|
||||
return format.acodec != "none" && format.vcodec == "none";
|
||||
});
|
||||
|
||||
if (audioFormats.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
#include <ytmusic.h>
|
||||
|
||||
@@ -124,3 +125,5 @@ private:
|
||||
|
||||
AsyncYTMusic *m_ytm;
|
||||
};
|
||||
|
||||
QUrl pickAudioUrl(const std::span<const video_info::Format> formats);
|
||||
@@ -17,7 +17,7 @@ import "dialogs"
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var info // VideoInfoExtractor object
|
||||
required property VideoInfoExtractor info // VideoInfoExtractor object
|
||||
required property var audio // Audio object
|
||||
required property string thumbnail
|
||||
readonly property bool isWidescreen: width >= Kirigami.Units.gridUnit * 50
|
||||
@@ -439,6 +439,38 @@ Item {
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: downloadButton
|
||||
|
||||
DownloadedWatcher {
|
||||
id: downloadedWatcher
|
||||
videoId: info.videoId
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
|
||||
Layout.maximumWidth: height
|
||||
Layout.preferredWidth: height
|
||||
onClicked: {
|
||||
if (downloadedWatcher.downloaded) {
|
||||
DownloadManager.deleteDownload(info.videoId)
|
||||
} else {
|
||||
DownloadManager.downloadSong(info.videoId)
|
||||
}
|
||||
}
|
||||
text: downloadedWatcher.downloaded ? i18n("Delete Download") : i18n("Download")
|
||||
icon.name: downloadedWatcher.downloaded ? "delete" : "download"
|
||||
icon.color: "white"
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: Kirigami.Settings.isMobile ? pressed : hovered
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: volumeButtonSmallScreen
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
@@ -208,7 +208,6 @@ Item {
|
||||
onTriggered: UserPlaylistModel.append(menu.videoId, menu.songTitle, menu.artists)
|
||||
}
|
||||
|
||||
|
||||
Controls.MenuSeparator{}
|
||||
|
||||
Controls.MenuItem {
|
||||
@@ -240,6 +239,23 @@ Item {
|
||||
icon.name: "media-playlist-append"
|
||||
onTriggered: playlistDialog.open()
|
||||
}
|
||||
|
||||
Controls.MenuItem {
|
||||
DownloadedWatcher {
|
||||
id: downloadedWatcher
|
||||
videoId: root.videoId
|
||||
}
|
||||
|
||||
text: downloadedWatcher.downloaded ? i18n("Delete download") : i18n("Download")
|
||||
icon.name: downloadedWatcher.downloaded ? "delete" : "download"
|
||||
onTriggered: {
|
||||
if (downloadedWatcher.downloaded) {
|
||||
DownloadManager.deleteDownload(root.videoId)
|
||||
} else {
|
||||
DownloadManager.downloadSong(videoId).then(() => showPassiveNotification("Download finished"))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Page specific actions //
|
||||
|
||||
Controls.MenuSeparator{ visible: instantiator.count !== 0 }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "downloadedsongsmodel.h"
|
||||
|
||||
#include "downloadmanager.h"
|
||||
#include "library.h"
|
||||
|
||||
namespace ranges = std::ranges;
|
||||
|
||||
DownloadedSongsModel::DownloadedSongsModel(QObject *parent)
|
||||
: QAbstractListModel{parent}
|
||||
{
|
||||
auto update = [this]() {
|
||||
auto future = Library::instance().downloadedSongs();
|
||||
QCoro::connect(std::move(future), this, [this](auto &&songs) {
|
||||
beginResetModel();
|
||||
m_songs = std::move(songs);
|
||||
endResetModel();
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2023 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 "library.h"
|
||||
|
||||
class DownloadedSongsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DownloadedSongsModel(QObject *parent = nullptr);
|
||||
|
||||
enum Role {
|
||||
Title,
|
||||
Artist,
|
||||
VideoId
|
||||
};
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
return {
|
||||
{ Title, "title" },
|
||||
{ Artist, "artist" },
|
||||
{ VideoId, "videoId" }
|
||||
};
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override {
|
||||
return parent.isValid() ? 0 : m_songs.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override {
|
||||
auto &song = m_songs[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case Role::Title:
|
||||
return song.title;
|
||||
case Role::Artist:
|
||||
return song.artist;
|
||||
case Role::VideoId:
|
||||
return song.videoId;
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Song> m_songs;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "downloadmanager.h"
|
||||
|
||||
#include <QUrl>
|
||||
#include <QFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include <QCoroNetworkReply>
|
||||
#include <QFile>
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
#include "library.h"
|
||||
#include "qdir.h"
|
||||
|
||||
constexpr auto BUFFER_SIZE = 4096;
|
||||
|
||||
DownloadManager::DownloadManager(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QCoro::QmlTask DownloadManager::downloadSong(const QString &videoId)
|
||||
{
|
||||
return download(videoId);
|
||||
}
|
||||
|
||||
void DownloadManager::deleteDownload(const QString &videoId)
|
||||
{
|
||||
QString directory = downloadDirectory();
|
||||
QFile::remove(directory % videoId);
|
||||
|
||||
auto future = Library::instance().markSongDownloaded(videoId, false);
|
||||
QCoro::connect(std::move(future), &Library::instance(), [videoId]() {
|
||||
Q_EMIT Library::instance().downloadedChanged(videoId);
|
||||
});
|
||||
}
|
||||
|
||||
QString DownloadManager::localPathOf(const QString &videoId)
|
||||
{
|
||||
return downloadDirectory() % "/" % videoId;
|
||||
}
|
||||
|
||||
QCoro::Task<> DownloadManager::download(const QString videoId)
|
||||
{
|
||||
auto info = co_await YTMusicThread::instance()->extractVideoInfo(videoId);
|
||||
|
||||
// If we don't know anything about the song yet, save what we know
|
||||
// This allows searching for the song if it was just downloaded
|
||||
QCoro::connect(Library::instance().getSong(videoId), this, [=](auto &&song) {
|
||||
if (!song) {
|
||||
Library::instance().addSong(videoId, QString::fromStdString(info.title), QString::fromStdString(info.artist), {});
|
||||
}
|
||||
});
|
||||
|
||||
QUrl url = pickAudioUrl(info.formats);
|
||||
//qDebug() << url;
|
||||
|
||||
auto *reply = Library::instance().nam().get(QNetworkRequest(url));
|
||||
|
||||
QString directory = downloadDirectory();
|
||||
QDir(directory).mkpath(".");
|
||||
|
||||
QString downloadLocation = directory % "/" % videoId % ".part";
|
||||
|
||||
auto *file = new QFile(downloadLocation);
|
||||
if (!file->open(QFile::WriteOnly | QFile::Truncate)) {
|
||||
qDebug() << "Failed to open" << downloadLocation << "for writing";
|
||||
co_return;
|
||||
}
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, [reply, file]() {
|
||||
QByteArray buffer;
|
||||
buffer.reserve(BUFFER_SIZE);
|
||||
|
||||
int read = 0;
|
||||
do {
|
||||
read = reply->read(buffer.data(), BUFFER_SIZE);
|
||||
file->write(buffer.data(), read);
|
||||
} while (read > 0);
|
||||
});
|
||||
co_await reply;
|
||||
|
||||
file->close();
|
||||
file->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QString location = directory % "/" % videoId;
|
||||
QFile::rename(downloadLocation, location);
|
||||
|
||||
Library::instance().markSongDownloaded(videoId, true);
|
||||
Q_EMIT Library::instance().downloadedChanged(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
QString DownloadManager::downloadDirectory()
|
||||
{
|
||||
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % "/downloads/";
|
||||
}
|
||||
|
||||
DownloadedWatcher::DownloadedWatcher(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
auto update = [this]() {
|
||||
qDebug() << "checking downloaded";
|
||||
auto future = Library::instance().songDownloaded(m_videoId);
|
||||
|
||||
QCoro::connect(std::move(future), this, [this](auto downloaded) {
|
||||
m_downloaded = downloaded;
|
||||
qDebug() << "downloaded" << m_downloaded;
|
||||
Q_EMIT downloadedChanged();
|
||||
});
|
||||
};
|
||||
|
||||
connect(this, &DownloadedWatcher::videoIdChanged, this, update);
|
||||
|
||||
connect(&Library::instance(), &Library::downloadedChanged, this, [=, this](const QString &videoId) {
|
||||
if (m_videoId == videoId) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2023 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 <QObject>
|
||||
|
||||
#include <QCoroTask>
|
||||
#include <QCoroQmlTask>
|
||||
|
||||
class DownloadManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DownloadManager(QObject *parent = nullptr);
|
||||
Q_INVOKABLE QCoro::QmlTask downloadSong(const QString &videoId);
|
||||
Q_INVOKABLE void deleteDownload(const QString &videoId);
|
||||
|
||||
static QString localPathOf(const QString &videoId);
|
||||
static QString downloadDirectory();
|
||||
|
||||
private:
|
||||
QCoro::Task<> download(const QString videoId);
|
||||
|
||||
};
|
||||
|
||||
class DownloadedWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString videoId MEMBER m_videoId NOTIFY videoIdChanged)
|
||||
Q_PROPERTY(bool downloaded MEMBER m_downloaded NOTIFY downloadedChanged)
|
||||
|
||||
public:
|
||||
explicit DownloadedWatcher(QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNAL void downloadedChanged();
|
||||
Q_SIGNAL void videoIdChanged();
|
||||
|
||||
private:
|
||||
bool m_downloaded = false;
|
||||
QString m_videoId;
|
||||
};
|
||||
+45
-6
@@ -105,12 +105,12 @@ void Library::refreshPlaybackHistory()
|
||||
{
|
||||
// playbackHistory
|
||||
auto future = m_database->getResults<PlayedSong>(
|
||||
"select * from played_songs natural join songs");
|
||||
"select * from played_songs natural join songs left join downloaded_songs on songs.video_id = downloaded_songs.video_id");
|
||||
m_playbackHistory = new PlaybackHistoryModel(std::move(future), this);
|
||||
|
||||
// mostPlayed
|
||||
auto future2 = m_database->getResults<PlayedSong>(
|
||||
"select * from played_songs natural join songs order by plays desc limit 10");
|
||||
"select * from played_songs natural join songs left join downloaded_songs on songs.video_id = downloaded_songs.video_id order by plays desc limit 10");
|
||||
m_mostPlayed = new PlaybackHistoryModel(std::move(future2), this);
|
||||
Q_EMIT playbackHistoryChanged();
|
||||
}
|
||||
@@ -161,8 +161,45 @@ QNetworkAccessManager &Library::nam()
|
||||
|
||||
QFuture<void> Library::addSong(const QString &videoId, const QString &title, const QString &artist, const QString &album)
|
||||
{
|
||||
// replace is used here to update songs from times when we didn't store artist and album
|
||||
return m_database->execute("insert or replace into songs (video_id, title, artist, album) values (?, ?, ?, ?)", videoId, title, artist, album);
|
||||
if (!videoId.isEmpty() && !title.isEmpty()) {
|
||||
// replace is used here to update songs from times when we didn't store artist and album
|
||||
return m_database->execute("insert or replace into songs (video_id, title, artist, album) values (?, ?, ?, ?)", videoId, title, artist, album);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QFuture<std::optional<Song>> Library::getSong(const QString &videoId)
|
||||
{
|
||||
return m_database->getResult<Song>("select * from songs where video_id = ?", videoId);
|
||||
}
|
||||
|
||||
QFuture<std::vector<Song> > Library::downloadedSongs()
|
||||
{
|
||||
return m_database->getResults<Song>(
|
||||
"select * from downloaded_songs natural join songs");
|
||||
}
|
||||
|
||||
QFuture<void> Library::markSongDownloaded(const QString &videoId, bool downloaded)
|
||||
{
|
||||
if (downloaded) {
|
||||
return m_database->execute(
|
||||
"insert or replace into downloaded_songs values (?)", videoId);
|
||||
} else {
|
||||
return m_database->execute(
|
||||
"delete from downloaded_songs where video_id = ?", videoId);
|
||||
}
|
||||
}
|
||||
|
||||
QCoro::Task<bool> Library::songDownloaded(const QString &videoId)
|
||||
{
|
||||
auto downloaded = co_await Library::instance().database().getResult<SingleValue<bool>>(
|
||||
"select count(*) > 0 from downloaded_songs where video_id = ?", videoId);
|
||||
|
||||
if (downloaded) {
|
||||
co_return downloaded->value;
|
||||
} else {
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackHistoryModel::PlaybackHistoryModel(QFuture<std::vector<PlayedSong>> &&songs, QObject *parent)
|
||||
@@ -440,10 +477,12 @@ LocalSearchModel::LocalSearchModel(QObject *parent) : PlaybackHistoryModel(paren
|
||||
{
|
||||
connect(this, &LocalSearchModel::searchQueryChanged, this, [this]() {
|
||||
auto resultFuture = Library::instance().database()
|
||||
.getResults<PlayedSong>("select * from played_songs natural join songs "
|
||||
.getResults<PlayedSong>("select songs.video_id, plays, title, artist, album, not downloaded_songs.video_id is null "
|
||||
"from songs left join played_songs on songs.video_id = played_songs.video_id "
|
||||
"left join downloaded_songs on songs.video_id = downloaded_songs.video_id "
|
||||
"where title like '%" % m_searchQuery % "%' "
|
||||
"order by plays desc limit 10");
|
||||
QCoro::connect(std::move(resultFuture), this, [this](auto results) {
|
||||
QCoro::connect(std::move(resultFuture), this, [this](auto &&results) {
|
||||
beginResetModel();
|
||||
m_playedSongs = results;
|
||||
endResetModel();
|
||||
|
||||
+11
-12
@@ -22,11 +22,6 @@ class WasPlayedWatcher;
|
||||
struct Song {
|
||||
using ColumnTypes = std::tuple<QString, QString, QString, QString>;
|
||||
|
||||
static Song fromSql(ColumnTypes tuple) {
|
||||
auto [videoId, title, artist, album] = tuple;
|
||||
return Song {videoId, title, artist, album};
|
||||
}
|
||||
|
||||
QString videoId;
|
||||
QString title;
|
||||
QString artist;
|
||||
@@ -56,18 +51,14 @@ private:
|
||||
};
|
||||
|
||||
struct PlayedSong {
|
||||
using ColumnTypes = std::tuple<QString, int, QString, QString, QString>;
|
||||
|
||||
static PlayedSong fromSql(ColumnTypes tuple) {
|
||||
auto [videoId, plays, title, artist, album] = tuple;
|
||||
return PlayedSong {videoId, title, artist, album, plays};
|
||||
}
|
||||
using ColumnTypes = std::tuple<QString, int, QString, QString, QString, bool>;
|
||||
|
||||
QString videoId;
|
||||
int plays;
|
||||
QString title;
|
||||
QString artist;
|
||||
QString album;
|
||||
int plays;
|
||||
bool downloaded;
|
||||
};
|
||||
|
||||
class PlaybackHistoryModel : public QAbstractListModel {
|
||||
@@ -183,6 +174,14 @@ public:
|
||||
return *m_database;
|
||||
}
|
||||
QFuture<void> addSong(const QString &videoId, const QString &title, const QString &artist, const QString &album);
|
||||
QFuture<std::optional<Song>> getSong(const QString &videoId);
|
||||
|
||||
QFuture<std::vector<Song>> downloadedSongs();
|
||||
|
||||
QFuture<void> markSongDownloaded(const QString &videoId, bool downloaded);
|
||||
QCoro::Task<bool> songDownloaded(const QString &videoId);
|
||||
|
||||
Q_SIGNAL void downloadedChanged(const QString &videoId);
|
||||
|
||||
private:
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <KCrash>
|
||||
#include <KAboutData>
|
||||
|
||||
#include <QCoroQml>
|
||||
|
||||
#include <mprisplugin.h>
|
||||
|
||||
#include "clipboard.h"
|
||||
@@ -34,6 +36,7 @@
|
||||
#include "localplaylistmodel.h"
|
||||
#include "localplaylistsmodel.h"
|
||||
#include "playlistimporter.h"
|
||||
#include "downloadmanager.h"
|
||||
|
||||
#include <ThreadedDatabase>
|
||||
|
||||
@@ -95,6 +98,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
MprisPlugin(&engine).registerTypes("org.nemomobile.qtmpris");
|
||||
QCoro::Qml::registerTypes();
|
||||
|
||||
qmlRegisterType<SearchModel>(URI, 1, 0, "SearchModel");
|
||||
qmlRegisterType<AlbumModel>(URI, 1, 0, "AlbumModel");
|
||||
@@ -125,9 +129,13 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonType<Clipboard>(URI, 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) {
|
||||
return new Clipboard();
|
||||
});
|
||||
qmlRegisterSingletonType<DownloadManager>(URI, 1, 0, "DownloadManager", [](QQmlEngine *, QJSEngine *) {
|
||||
return new DownloadManager();
|
||||
});
|
||||
|
||||
qmlRegisterSingletonInstance<Library>(URI, 1, 0, "Library", &Library::instance());
|
||||
qmlRegisterType<ThumbnailSource>(URI, 1, 0, "ThumbnailSource");
|
||||
qmlRegisterType<DownloadedWatcher>(URI, 1, 0, "DownloadedWatcher");
|
||||
qmlRegisterAnonymousType<FavouriteWatcher>(URI, 1);
|
||||
qmlRegisterAnonymousType<WasPlayedWatcher>(URI, 1);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
|
||||
--
|
||||
-- SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
drop table downloaded_songs;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
|
||||
--
|
||||
-- SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
create table downloaded_songs (video_id Text primary key not null);
|
||||
@@ -47,5 +47,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<file alias="AlbumCoverItem.qml">contents/ui/AlbumCoverItem.qml</file>
|
||||
<file alias="components/ActionButton.qml">contents/ui/components/ActionButton.qml</file>
|
||||
<file alias="components/DoubleActionButton.qml">contents/ui/components/DoubleActionButton.qml</file>
|
||||
<file>migrations/2023-08-10-212523_downloads/down.sql</file>
|
||||
<file>migrations/2023-08-10-212523_downloads/up.sql</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -31,13 +31,29 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
|
||||
m_playlist = std::move(playlist);
|
||||
endResetModel();
|
||||
setCurrentVideoId({});
|
||||
if (m_shuffle) {
|
||||
shufflePlaylist();
|
||||
|
||||
// reset shuffle
|
||||
setShuffle(false);
|
||||
}
|
||||
if (!m_playlist.tracks.empty()) {
|
||||
if (m_playlist.tracks.empty()) {
|
||||
// offline mode
|
||||
QCoro::connect(Library::instance().getSong(m_initialVideoId), this, [this](auto song) {
|
||||
if (song) {
|
||||
watch::Playlist::Track track;
|
||||
track.video_id = m_initialVideoId.toStdString();
|
||||
track.title = song->title.toStdString();
|
||||
track.artists = {meta::Artist {song->artist.toStdString(), {}}};
|
||||
|
||||
beginResetModel();
|
||||
m_playlist.tracks.push_back(std::move(track));
|
||||
endResetModel();
|
||||
setCurrentVideoId(QString::fromStdString(m_playlist.tracks.front().video_id));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (m_shuffle) {
|
||||
shufflePlaylist();
|
||||
|
||||
// reset shuffle
|
||||
setShuffle(false);
|
||||
}
|
||||
setCurrentVideoId(QString::fromStdString(m_playlist.tracks.front().video_id));
|
||||
}
|
||||
};
|
||||
|
||||
+50
-28
@@ -5,11 +5,16 @@
|
||||
#include "videoinfoextractor.h"
|
||||
|
||||
#include "asyncytmusic.h"
|
||||
#include "library.h"
|
||||
#include "downloadmanager.h"
|
||||
|
||||
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(&m_thumbnailSource, &ThumbnailSource::cachedPathChanged, this, &VideoInfoExtractor::songChanged);
|
||||
|
||||
connect(this, &VideoInfoExtractor::videoIdChanged, this, [this] {
|
||||
qDebug() << "video id changed to" << m_videoId;
|
||||
if (m_videoId.isEmpty()) {
|
||||
m_videoInfo = {};
|
||||
Q_EMIT songChanged();
|
||||
@@ -18,39 +23,44 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
|
||||
|
||||
setLoading(true);
|
||||
|
||||
auto future = YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
|
||||
QCoro::connect(std::move(future), this, [this](const video_info::VideoInfo &videoInfo) {
|
||||
m_videoInfo = videoInfo;
|
||||
setLoading(false);
|
||||
Q_EMIT songChanged();
|
||||
QCoro::connect(Library::instance().songDownloaded(m_videoId), this, [this](bool downloaded) {
|
||||
m_downloaded = downloaded;
|
||||
if (downloaded) {
|
||||
m_thumbnailSource.setVideoId(m_videoId);
|
||||
|
||||
QCoro::connect(Library::instance().getSong(m_videoId), this, [this](auto &&song) {
|
||||
if (song) {
|
||||
m_localInfo = std::move(*song);
|
||||
setLoading(false);
|
||||
Q_EMIT songChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto future = YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
|
||||
QCoro::connect(std::move(future), this, [this](video_info::VideoInfo &&videoInfo) {
|
||||
if (videoInfo.id.empty()) {
|
||||
return;
|
||||
}
|
||||
m_videoInfo = std::move(videoInfo);
|
||||
setLoading(false);
|
||||
Q_EMIT songChanged();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QUrl VideoInfoExtractor::audioUrl() const
|
||||
{
|
||||
if (m_videoInfo.formats.empty()) {
|
||||
if (m_videoId.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<video_info::Format> audioFormats;
|
||||
|
||||
// filter audio only formats
|
||||
std::copy_if(m_videoInfo.formats.begin(), m_videoInfo.formats.end(), std::back_inserter(audioFormats),
|
||||
[](const video_info::Format &format) {
|
||||
return format.acodec != "none" && format.vcodec == "none";
|
||||
});
|
||||
|
||||
if (audioFormats.empty()) {
|
||||
return {};
|
||||
if (!m_videoInfo) {
|
||||
return QUrl::fromLocalFile(DownloadManager::localPathOf(m_videoId));
|
||||
} else {
|
||||
return pickAudioUrl(m_videoInfo->formats);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
QString VideoInfoExtractor::videoId() const
|
||||
@@ -69,22 +79,34 @@ void VideoInfoExtractor::setVideoId(const QString &videoId)
|
||||
|
||||
QString VideoInfoExtractor::title() const
|
||||
{
|
||||
return QString::fromStdString(m_videoInfo.title);
|
||||
if (!m_videoInfo) {
|
||||
return m_localInfo.title;
|
||||
}
|
||||
return QString::fromStdString(m_videoInfo->title);
|
||||
}
|
||||
|
||||
QString VideoInfoExtractor::artist() const
|
||||
{
|
||||
return QString::fromStdString(m_videoInfo.artist);
|
||||
if (!m_videoInfo) {
|
||||
return m_localInfo.artist;
|
||||
}
|
||||
return QString::fromStdString(m_videoInfo->artist);
|
||||
}
|
||||
|
||||
QString VideoInfoExtractor::channel() const
|
||||
{
|
||||
return QString::fromStdString(m_videoInfo.channel);
|
||||
if (!m_videoInfo) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromStdString(m_videoInfo->channel);
|
||||
}
|
||||
|
||||
QString VideoInfoExtractor::thumbnail() const
|
||||
QUrl VideoInfoExtractor::thumbnail() const
|
||||
{
|
||||
return QString::fromStdString(m_videoInfo.thumbnail);
|
||||
if (!m_videoInfo) {
|
||||
return m_thumbnailSource.cachedPath();
|
||||
}
|
||||
return QUrl(QString::fromStdString(m_videoInfo->thumbnail));
|
||||
}
|
||||
|
||||
bool VideoInfoExtractor::loading() const
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include <ytmusic.h>
|
||||
#include "library.h"
|
||||
#include "thumbnailsource.h"
|
||||
|
||||
class VideoInfoExtractor : public QObject
|
||||
{
|
||||
@@ -19,7 +21,7 @@ class VideoInfoExtractor : public QObject
|
||||
Q_PROPERTY(QString title READ title NOTIFY songChanged)
|
||||
Q_PROPERTY(QString artist READ artist NOTIFY songChanged) // may be empty if it is a video
|
||||
Q_PROPERTY(QString channel READ channel NOTIFY songChanged)
|
||||
Q_PROPERTY(QString thumbnail READ thumbnail NOTIFY songChanged)
|
||||
Q_PROPERTY(QUrl thumbnail READ thumbnail NOTIFY songChanged)
|
||||
Q_PROPERTY(bool loading READ loading WRITE setLoading NOTIFY songChanged)
|
||||
|
||||
public:
|
||||
@@ -37,7 +39,7 @@ public:
|
||||
|
||||
QString channel() const;
|
||||
|
||||
QString thumbnail() const;
|
||||
QUrl thumbnail() const;
|
||||
|
||||
bool loading() const;
|
||||
void setLoading(bool loading);
|
||||
@@ -48,5 +50,10 @@ public:
|
||||
private:
|
||||
bool m_loading = false;
|
||||
QString m_videoId;
|
||||
video_info::VideoInfo m_videoInfo;
|
||||
std::optional<video_info::VideoInfo> m_videoInfo;
|
||||
|
||||
// Fallback data used when offline
|
||||
bool m_downloaded = false;
|
||||
Song m_localInfo;
|
||||
ThumbnailSource m_thumbnailSource;
|
||||
};
|
||||
@@ -496,6 +496,7 @@ video_info::VideoInfo YTMusic::extract_video_info(const std::string &video_id) c
|
||||
using namespace pybind11::literals;
|
||||
|
||||
const auto info = d->get_ytdl().attr("extract_info")(video_id, "download"_a=py::bool_(false));
|
||||
pyPrintPretty(info);
|
||||
|
||||
return {
|
||||
info["id"].cast<std::string>(),
|
||||
|
||||
@@ -242,6 +242,8 @@ struct Format {
|
||||
std::string acodec;
|
||||
|
||||
// More, but not interesting for us right now
|
||||
|
||||
// TODO parse downloader_options, http_headers
|
||||
};
|
||||
|
||||
struct VideoInfo {
|
||||
|
||||
Reference in new issue
Block a user