Compare commits

...
Author SHA1 Message Date
Jonah Brüchert cc3710c200 wip 2023-12-20 02:26:58 +01:00
Jonah Brüchert 48cf377194 Disallow building with Qt5 2023-12-19 04:36:05 +01:00
Jonah Brüchert 07f5043016 wip 2023-12-19 04:35:35 +01:00
Jonah Brüchert 2819ed9501 videoinfoextractor: Make sure not to return invalid urls 2023-12-19 04:35:35 +01:00
Jonah Brüchert 0c0dca262d wip 2023-12-19 04:35:33 +01:00
Jonah Brüchert b4a2331097 LocalSearchModel: Include not-played songs 2023-12-19 04:34:17 +01:00
Jonah Brüchert 4ea2fa3a46 Fix songs not autoplaying 2023-12-19 04:34:15 +01:00
Jonah Brüchert 1d987c14cd Add download button to player 2023-12-19 04:33:31 +01:00
Jonah Brüchert 35f38e8598 Wire up VideoInfoExtractor to local metadata 2023-12-19 04:33:29 +01:00
Jonah Brüchert e015301863 Play local version of song if available 2023-12-19 04:29:44 +01:00
Jonah Brüchert 94a8edeef3 WIP: Implement downloads
Scope for now:
 * [x] Store downloads in internal directory
 * [ ] play local version if available instead of streaming
 * [ ] allow to play local songs from the library and local search
   results while offline
2023-12-19 04:26:16 +01:00
21 changed files with 502 additions and 68 deletions

No files matched your search

+5 -11
View File
@@ -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)
+4
View File
@@ -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
)
+26
View File
@@ -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));
}
+3
View File
@@ -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);
+33 -1
View File
@@ -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
+17 -1
View File
@@ -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 }
+25
View File
@@ -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();
}
+56
View File
@@ -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;
};
+127
View File
@@ -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();
}
+45
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+8
View File
@@ -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);
+2
View File
@@ -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>
+22 -6
View File
@@ -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
View File
@@ -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
+10 -3
View File
@@ -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;
};
+1
View File
@@ -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>(),
+2
View File
@@ -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 {