Fetch metadata and allow editing for stream songs
This commit is contained in:
committed by
Jonas Kvinge
parent
ea629aedd1
commit
a71e5b170b
@@ -1447,6 +1447,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.cpp
|
||||
src/spotify/spotifyrequest.cpp
|
||||
src/spotify/spotifyfavoriterequest.cpp
|
||||
src/spotify/spotifymetadatarequest.cpp
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1454,6 +1455,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.h
|
||||
src/spotify/spotifyrequest.h
|
||||
src/spotify/spotifyfavoriterequest.h
|
||||
src/spotify/spotifymetadatarequest.h
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
UI
|
||||
@@ -1468,6 +1470,7 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.cpp
|
||||
src/qobuz/qobuzstreamurlrequest.cpp
|
||||
src/qobuz/qobuzfavoriterequest.cpp
|
||||
src/qobuz/qobuzmetadatarequest.cpp
|
||||
src/qobuz/qobuzcredentialfetcher.cpp
|
||||
src/settings/qobuzsettingspage.cpp
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
@@ -1478,6 +1481,7 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.h
|
||||
src/qobuz/qobuzstreamurlrequest.h
|
||||
src/qobuz/qobuzfavoriterequest.h
|
||||
src/qobuz/qobuzmetadatarequest.h
|
||||
src/qobuz/qobuzcredentialfetcher.h
|
||||
src/settings/qobuzsettingspage.h
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
|
||||
@@ -173,9 +173,12 @@
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
# include "spotify/spotifyservice.h"
|
||||
# include "spotify/spotifymetadatarequest.h"
|
||||
# include "constants/spotifysettings.h"
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
# include "qobuz/qobuzservice.h"
|
||||
# include "qobuz/qobuzmetadatarequest.h"
|
||||
# include "constants/qobuzsettings.h"
|
||||
#endif
|
||||
|
||||
@@ -379,8 +382,10 @@ MainWindow::MainWindow(Application *app,
|
||||
playlist_add_to_another_(nullptr),
|
||||
playlistitem_actions_separator_(nullptr),
|
||||
playlist_rescan_songs_(nullptr),
|
||||
playlist_fetch_metadata_(nullptr),
|
||||
track_position_timer_(new QTimer(this)),
|
||||
track_slider_timer_(new QTimer(this)),
|
||||
metadata_queue_timer_(new QTimer(this)),
|
||||
keep_running_(false),
|
||||
playing_widget_(true),
|
||||
#ifdef HAVE_DBUS
|
||||
@@ -452,6 +457,10 @@ MainWindow::MainWindow(Application *app,
|
||||
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
||||
QObject::connect(track_slider_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackSliderPosition);
|
||||
|
||||
metadata_queue_timer_->setInterval(200ms); // 200ms between requests to avoid rate limiting
|
||||
metadata_queue_timer_->setSingleShot(true);
|
||||
QObject::connect(metadata_queue_timer_, &QTimer::timeout, this, &MainWindow::ProcessMetadataQueue);
|
||||
|
||||
// Start initializing the player
|
||||
qLog(Debug) << "Initializing player";
|
||||
app_->player()->SetAnalyzer(ui_->analyzer);
|
||||
@@ -812,6 +821,8 @@ MainWindow::MainWindow(Application *app,
|
||||
#endif
|
||||
playlist_rescan_songs_ = playlist_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Rescan song(s)..."), this, &MainWindow::RescanSongs);
|
||||
playlist_menu_->addAction(playlist_rescan_songs_);
|
||||
playlist_fetch_metadata_ = playlist_menu_->addAction(IconLoader::Load(u"download"_s), tr("Fetch metadata from service"), this, &MainWindow::FetchStreamingMetadata);
|
||||
playlist_menu_->addAction(playlist_fetch_metadata_);
|
||||
playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
|
||||
playlist_menu_->addSeparator();
|
||||
playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy URL(s)..."), this, &MainWindow::PlaylistCopyUrl);
|
||||
@@ -1995,6 +2006,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
int in_skipped = 0;
|
||||
int not_in_skipped = 0;
|
||||
int local_songs = 0;
|
||||
int streaming_songs = 0;
|
||||
|
||||
for (const QModelIndex &idx : selection) {
|
||||
|
||||
@@ -2004,7 +2016,13 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
||||
if (!item) continue;
|
||||
|
||||
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
|
||||
if (item->EffectiveMetadata().url().isLocalFile()) {
|
||||
++local_songs;
|
||||
}
|
||||
|
||||
if (item->EffectiveMetadata().is_stream_service()) {
|
||||
++streaming_songs;
|
||||
}
|
||||
|
||||
if (item->EffectiveMetadata().has_cue()) {
|
||||
cue_selected = true;
|
||||
@@ -2032,6 +2050,9 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
playlist_rescan_songs_->setEnabled(local_songs > 0 && editable > 0);
|
||||
playlist_rescan_songs_->setVisible(local_songs > 0 && editable > 0);
|
||||
|
||||
playlist_fetch_metadata_->setEnabled(streaming_songs > 0);
|
||||
playlist_fetch_metadata_->setVisible(streaming_songs > 0);
|
||||
|
||||
ui_->action_add_files_to_transcoder->setEnabled(local_songs > 0 && editable > 0);
|
||||
ui_->action_add_files_to_transcoder->setVisible(local_songs > 0 && editable > 0);
|
||||
|
||||
@@ -2243,9 +2264,23 @@ void MainWindow::EditTracks() {
|
||||
void MainWindow::EditTagDialogAccepted() {
|
||||
|
||||
const PlaylistItemPtrList items = edit_tag_dialog_->playlist_items();
|
||||
for (PlaylistItemPtr item : items) {
|
||||
const SongList songs = edit_tag_dialog_->songs();
|
||||
|
||||
if (items.count() != songs.count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.count(); ++i) {
|
||||
PlaylistItemPtr item = items[i];
|
||||
const Song &updated_song = songs[i];
|
||||
// For stream tracks, apply the metadata directly since there's no file to reload from
|
||||
if (updated_song.is_stream_service()) {
|
||||
item->SetOriginalMetadata(updated_song);
|
||||
}
|
||||
else {
|
||||
item->Reload();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is really lame but we don't know what rows have changed.
|
||||
ui_->playlist->view()->update();
|
||||
@@ -2319,8 +2354,8 @@ void MainWindow::SelectionSetValue() {
|
||||
QObject::disconnect(*connection);
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
else if (song.source() == Song::Source::Stream) {
|
||||
app_->playlist_manager()->current()->setData(source_index, column_value, 0);
|
||||
else if (song.is_stream()) {
|
||||
app_->playlist_manager()->current()->setData(source_index.sibling(source_index.row(), static_cast<int>(column)), column_value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3404,3 +3439,172 @@ void MainWindow::FocusSearchField() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::FetchStreamingMetadata() {
|
||||
|
||||
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
||||
for (const QModelIndex &proxy_index : proxy_indexes) {
|
||||
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
||||
if (!source_index.isValid()) continue;
|
||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
||||
if (!item) continue;
|
||||
|
||||
const Song &song = item->EffectiveMetadata();
|
||||
const QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
||||
QString track_id;
|
||||
|
||||
#ifdef HAVE_QOBUZ
|
||||
if (song.source() == Song::Source::Qobuz) {
|
||||
track_id = song.song_id();
|
||||
// song_id() may be empty if not persisted, fall back to URL path
|
||||
if (track_id.isEmpty()) {
|
||||
track_id = song.url().path();
|
||||
}
|
||||
if (track_id.isEmpty()) {
|
||||
qLog(Error) << "Failed to fetch Qobuz metadata: No track ID";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
if (song.source() == Song::Source::Spotify) {
|
||||
track_id = song.song_id();
|
||||
// song_id() may be empty if not persisted, fall back to parsing URL
|
||||
if (track_id.isEmpty() && song.url().scheme() == "spotify"_L1 && song.url().path().startsWith(u"track:"_s)) {
|
||||
track_id = song.url().path().mid(6);
|
||||
}
|
||||
if (track_id.isEmpty()) {
|
||||
qLog(Error) << "Failed to fetch Spotify metadata: No track ID";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!track_id.isEmpty()) {
|
||||
metadata_queue_.append({song.source(), track_id, persistent_index});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Start processing the queue if it's not already running
|
||||
if (!metadata_queue_.isEmpty() && !metadata_queue_timer_->isActive()) {
|
||||
ProcessMetadataQueue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::ProcessMetadataQueue() {
|
||||
|
||||
if (metadata_queue_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MetadataQueueEntry metadata_queue_entry = metadata_queue_.takeFirst();
|
||||
|
||||
#ifdef HAVE_QOBUZ
|
||||
if (metadata_queue_entry.source == Song::Source::Qobuz) {
|
||||
if (QobuzServicePtr qobuz_service = app_->streaming_services()->Service<QobuzService>()) {
|
||||
QobuzMetadataRequest *request = new QobuzMetadataRequest(qobuz_service.get(), qobuz_service->network(), this);
|
||||
QObject::connect(request, &QobuzMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
||||
Q_UNUSED(received_track_id);
|
||||
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
||||
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
||||
if (playlist_item) {
|
||||
const Song old_song = playlist_item->OriginalMetadata();
|
||||
Song updated_song = old_song;
|
||||
// Update all metadata fields from the fetched song
|
||||
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
||||
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
||||
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
||||
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
||||
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
||||
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
||||
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
||||
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
||||
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
||||
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
||||
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
||||
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
||||
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
||||
playlist_item->SetOriginalMetadata(updated_song);
|
||||
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
||||
}
|
||||
}
|
||||
request->deleteLater();
|
||||
// Process next item in queue
|
||||
if (!metadata_queue_.isEmpty()) {
|
||||
metadata_queue_timer_->start();
|
||||
}
|
||||
});
|
||||
QObject::connect(request, &QobuzMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
||||
Q_UNUSED(failed_track_id);
|
||||
qLog(Error) << "Failed to fetch Qobuz metadata:" << error;
|
||||
request->deleteLater();
|
||||
// Process next item in queue
|
||||
if (!metadata_queue_.isEmpty()) {
|
||||
metadata_queue_timer_->start();
|
||||
}
|
||||
});
|
||||
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
if (metadata_queue_entry.source == Song::Source::Spotify) {
|
||||
if (SpotifyServicePtr spotify_service = app_->streaming_services()->Service<SpotifyService>()) {
|
||||
SpotifyMetadataRequest *request = new SpotifyMetadataRequest(spotify_service.get(), app_->network(), this);
|
||||
QObject::connect(request, &SpotifyMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
||||
Q_UNUSED(received_track_id);
|
||||
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
||||
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
||||
if (playlist_item) {
|
||||
const Song old_song = playlist_item->OriginalMetadata();
|
||||
Song updated_song = old_song;
|
||||
// Update all metadata fields from the fetched song
|
||||
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
||||
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
||||
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
||||
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
||||
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
||||
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
||||
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
||||
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
||||
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
||||
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
||||
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
||||
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
||||
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
||||
playlist_item->SetOriginalMetadata(updated_song);
|
||||
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
||||
}
|
||||
}
|
||||
request->deleteLater();
|
||||
// Process next item in queue
|
||||
if (!metadata_queue_.isEmpty()) {
|
||||
metadata_queue_timer_->start();
|
||||
}
|
||||
});
|
||||
QObject::connect(request, &SpotifyMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
||||
Q_UNUSED(failed_track_id);
|
||||
qLog(Error) << "Failed to fetch Spotify metadata:" << error;
|
||||
request->deleteLater();
|
||||
// Process next item in queue
|
||||
if (!metadata_queue_.isEmpty()) {
|
||||
metadata_queue_timer_->start();
|
||||
}
|
||||
});
|
||||
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we get here, the source wasn't handled - try the next item
|
||||
if (!metadata_queue_.isEmpty()) {
|
||||
metadata_queue_timer_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -276,6 +276,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||
|
||||
void FetchStreamingMetadata();
|
||||
void ProcessMetadataQueue();
|
||||
|
||||
public Q_SLOTS:
|
||||
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||
void Raise();
|
||||
@@ -379,11 +382,13 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
QList<QAction*> playlistitem_actions_;
|
||||
QAction *playlistitem_actions_separator_;
|
||||
QAction *playlist_rescan_songs_;
|
||||
QAction *playlist_fetch_metadata_;
|
||||
|
||||
QModelIndex playlist_menu_index_;
|
||||
|
||||
QTimer *track_position_timer_;
|
||||
QTimer *track_slider_timer_;
|
||||
QTimer *metadata_queue_timer_;
|
||||
|
||||
bool keep_running_;
|
||||
bool playing_widget_;
|
||||
@@ -407,6 +412,14 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
bool playlists_loaded_;
|
||||
bool delete_files_;
|
||||
std::optional<CommandlineOptions> options_;
|
||||
|
||||
class MetadataQueueEntry {
|
||||
public:
|
||||
Song::Source source;
|
||||
QString track_id;
|
||||
QPersistentModelIndex persistent_index;
|
||||
};
|
||||
QList<MetadataQueueEntry> metadata_queue_;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -686,8 +686,9 @@ const QString &Song::playlist_effective_albumartistsort() const { return is_comp
|
||||
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
||||
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
|
||||
bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
||||
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
|
||||
bool Song::is_stream_service() const { return d->source_ == Source::Subsonic || d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
||||
bool Song::is_stream() const { return is_radio() || is_stream_service(); }
|
||||
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
|
||||
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
|
||||
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
|
||||
@@ -956,7 +957,7 @@ QString Song::PrettyRating() const {
|
||||
}
|
||||
|
||||
bool Song::IsEditable() const {
|
||||
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
|
||||
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || is_stream());
|
||||
}
|
||||
|
||||
bool Song::IsFileInfoEqual(const Song &other) const {
|
||||
|
||||
@@ -407,8 +407,9 @@ class Song {
|
||||
bool is_metadata_good() const;
|
||||
bool is_local_collection_song() const;
|
||||
bool is_linked_collection_song() const;
|
||||
bool is_stream() const;
|
||||
bool is_radio() const;
|
||||
bool is_stream_service() const;
|
||||
bool is_stream() const;
|
||||
bool is_cdda() const;
|
||||
bool is_compilation() const;
|
||||
bool stream_url_can_expire() const;
|
||||
|
||||
@@ -411,6 +411,17 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) {
|
||||
|
||||
}
|
||||
|
||||
SongList EditTagDialog::songs() const {
|
||||
|
||||
SongList result;
|
||||
for (const Data &d : data_) {
|
||||
result << d.current_;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
bool EditTagDialog::SetLoading(const QString &message) {
|
||||
|
||||
const bool loading = !message.isEmpty();
|
||||
@@ -1399,6 +1410,12 @@ void EditTagDialog::SaveData() {
|
||||
}
|
||||
|
||||
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
|
||||
// For streaming tracks, skip tag writing since there's no local file.
|
||||
// The metadata will be applied directly to the playlist item in MainWindow::EditTagDialogAccepted.
|
||||
if (ref.current_.is_stream()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not to confuse the collection model.
|
||||
if (ref.current_.track() <= 0) { ref.current_.set_track(-1); }
|
||||
if (ref.current_.disc() <= 0) { ref.current_.set_disc(-1); }
|
||||
|
||||
@@ -85,7 +85,7 @@ class EditTagDialog : public QDialog {
|
||||
void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList());
|
||||
|
||||
PlaylistItemPtrList playlist_items() const { return playlist_items_; }
|
||||
|
||||
SongList songs() const;
|
||||
void accept() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -474,8 +474,10 @@ bool Playlist::setData(const QModelIndex &idx, const QVariant &value, const int
|
||||
QObject::disconnect(*connection);
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
else if (song.is_radio()) {
|
||||
else if (song.is_stream()) {
|
||||
item->SetOriginalMetadata(song);
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
||||
Q_EMIT EditingFinished(id_, idx);
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
|
||||
230
src/qobuz/qobuzmetadatarequest.cpp
Normal file
230
src/qobuz/qobuzmetadatarequest.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/song.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzmetadatarequest.h"
|
||||
|
||||
namespace {
|
||||
constexpr qint64 kNsecPerSec = 1000000000LL;
|
||||
}
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
QobuzMetadataRequest::QobuzMetadataRequest(QobuzService *service, const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: QobuzBaseRequest(service, network, parent) {}
|
||||
|
||||
void QobuzMetadataRequest::FetchTrackMetadata(const QString &track_id) {
|
||||
|
||||
if (!authenticated()) {
|
||||
Q_EMIT MetadataFailure(track_id, tr("Not authenticated"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (track_id.isEmpty()) {
|
||||
Q_EMIT MetadataFailure(track_id, tr("No track ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParamList params = ParamList() << Param(u"track_id"_s, track_id);
|
||||
|
||||
QNetworkReply *reply = CreateRequest(u"track/get"_s, params);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, track_id]() {
|
||||
TrackMetadataReceived(reply, track_id);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void QobuzMetadataRequest::TrackMetadataReceived(QNetworkReply *reply, const QString &track_id) {
|
||||
|
||||
if (!replies_.contains(reply)) {
|
||||
qLog(Debug) << "Qobuz: Reply not in replies_ list for track" << track_id;
|
||||
return;
|
||||
}
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
JsonObjectResult result = ParseJsonObject(reply);
|
||||
if (result.error_code != JsonBaseRequest::ErrorCode::Success) {
|
||||
Error(result.error_message);
|
||||
Q_EMIT MetadataFailure(track_id, result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject &json_obj = result.json_object;
|
||||
|
||||
Song song;
|
||||
song.set_source(Song::Source::Qobuz);
|
||||
|
||||
// Parse song ID
|
||||
QString song_id;
|
||||
if (json_obj["id"_L1].isString()) {
|
||||
song_id = json_obj["id"_L1].toString();
|
||||
}
|
||||
else {
|
||||
song_id = QString::number(json_obj["id"_L1].toInt());
|
||||
}
|
||||
song.set_song_id(song_id);
|
||||
|
||||
// Parse basic track info
|
||||
if (json_obj.contains("title"_L1)) {
|
||||
song.set_title(json_obj["title"_L1].toString());
|
||||
}
|
||||
if (json_obj.contains("track_number"_L1)) {
|
||||
song.set_track(json_obj["track_number"_L1].toInt());
|
||||
}
|
||||
if (json_obj.contains("media_number"_L1)) {
|
||||
song.set_disc(json_obj["media_number"_L1].toInt());
|
||||
}
|
||||
if (json_obj.contains("duration"_L1)) {
|
||||
song.set_length_nanosec(json_obj["duration"_L1].toInt() * kNsecPerSec);
|
||||
}
|
||||
if (json_obj.contains("copyright"_L1)) {
|
||||
song.set_comment(json_obj["copyright"_L1].toString());
|
||||
}
|
||||
if (json_obj.contains("composer"_L1)) {
|
||||
QJsonValue value_composer = json_obj["composer"_L1];
|
||||
if (value_composer.isObject()) {
|
||||
QJsonObject obj_composer = value_composer.toObject();
|
||||
if (obj_composer.contains("name"_L1)) {
|
||||
song.set_composer(obj_composer["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json_obj.contains("performer"_L1)) {
|
||||
QJsonValue value_performer = json_obj["performer"_L1];
|
||||
if (value_performer.isObject()) {
|
||||
QJsonObject obj_performer = value_performer.toObject();
|
||||
if (obj_performer.contains("name"_L1)) {
|
||||
song.set_performer(obj_performer["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse album info (includes artist, cover, genre)
|
||||
if (json_obj.contains("album"_L1)) {
|
||||
QJsonValue value_album = json_obj["album"_L1];
|
||||
if (value_album.isObject()) {
|
||||
QJsonObject obj_album = value_album.toObject();
|
||||
|
||||
if (obj_album.contains("id"_L1)) {
|
||||
QString album_id;
|
||||
if (obj_album["id"_L1].isString()) {
|
||||
album_id = obj_album["id"_L1].toString();
|
||||
}
|
||||
else {
|
||||
album_id = QString::number(obj_album["id"_L1].toInt());
|
||||
}
|
||||
song.set_album_id(album_id);
|
||||
}
|
||||
|
||||
if (obj_album.contains("title"_L1)) {
|
||||
song.set_album(obj_album["title"_L1].toString());
|
||||
}
|
||||
|
||||
// Artist from album
|
||||
if (obj_album.contains("artist"_L1)) {
|
||||
QJsonValue value_artist = obj_album["artist"_L1];
|
||||
if (value_artist.isObject()) {
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (obj_artist.contains("id"_L1)) {
|
||||
QString artist_id;
|
||||
if (obj_artist["id"_L1].isString()) {
|
||||
artist_id = obj_artist["id"_L1].toString();
|
||||
}
|
||||
else {
|
||||
artist_id = QString::number(obj_artist["id"_L1].toInt());
|
||||
}
|
||||
song.set_artist_id(artist_id);
|
||||
}
|
||||
if (obj_artist.contains("name"_L1)) {
|
||||
song.set_artist(obj_artist["name"_L1].toString());
|
||||
song.set_albumartist(obj_artist["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cover image
|
||||
if (obj_album.contains("image"_L1)) {
|
||||
QJsonValue value_image = obj_album["image"_L1];
|
||||
if (value_image.isObject()) {
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (obj_image.contains("large"_L1)) {
|
||||
QString cover_url = obj_image["large"_L1].toString();
|
||||
if (!cover_url.isEmpty()) {
|
||||
song.set_art_automatic(QUrl(cover_url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Genre
|
||||
if (obj_album.contains("genre"_L1)) {
|
||||
QJsonValue value_genre = obj_album["genre"_L1];
|
||||
if (value_genre.isObject()) {
|
||||
QJsonObject obj_genre = value_genre.toObject();
|
||||
if (obj_genre.contains("name"_L1)) {
|
||||
song.set_genre(obj_genre["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release date / year
|
||||
if (obj_album.contains("released_at"_L1)) {
|
||||
qint64 released_at = obj_album["released_at"_L1].toVariant().toLongLong();
|
||||
if (released_at > 0) {
|
||||
QDateTime datetime = QDateTime::fromSecsSinceEpoch(released_at);
|
||||
song.set_year(datetime.date().year());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
song.set_valid(true);
|
||||
|
||||
qLog(Debug) << "Qobuz: Track metadata received for" << track_id
|
||||
<< "- title:" << song.title()
|
||||
<< "- artist:" << song.artist()
|
||||
<< "- album:" << song.album()
|
||||
<< "- genre:" << song.genre();
|
||||
|
||||
Q_EMIT MetadataReceived(track_id, song);
|
||||
|
||||
}
|
||||
|
||||
void QobuzMetadataRequest::Error(const QString &error_message, const QVariant &debug_output) {
|
||||
|
||||
qLog(Error) << "Qobuz:" << error_message;
|
||||
if (debug_output.isValid()) qLog(Debug) << debug_output;
|
||||
|
||||
}
|
||||
55
src/qobuz/qobuzmetadatarequest.h
Normal file
55
src/qobuz/qobuzmetadatarequest.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef QOBUZMETADATAREQUEST_H
|
||||
#define QOBUZMETADATAREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class QobuzService;
|
||||
|
||||
class QobuzMetadataRequest : public QobuzBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzMetadataRequest(QobuzService *service, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
|
||||
void FetchTrackMetadata(const QString &track_id);
|
||||
|
||||
Q_SIGNALS:
|
||||
void MetadataReceived(QString track_id, Song song);
|
||||
void MetadataFailure(QString track_id, QString error);
|
||||
|
||||
private Q_SLOTS:
|
||||
void TrackMetadataReceived(QNetworkReply *reply, const QString &track_id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
|
||||
};
|
||||
|
||||
#endif // QOBUZMETADATAREQUEST_H
|
||||
231
src/spotify/spotifymetadatarequest.cpp
Normal file
231
src/spotify/spotifymetadatarequest.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/song.h"
|
||||
#include "spotifyservice.h"
|
||||
#include "spotifymetadatarequest.h"
|
||||
|
||||
namespace {
|
||||
constexpr qint64 kNsecPerMsec = 1000000LL;
|
||||
}
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
SpotifyMetadataRequest::SpotifyMetadataRequest(SpotifyService *service, const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: SpotifyBaseRequest(service, network, parent) {}
|
||||
|
||||
void SpotifyMetadataRequest::FetchTrackMetadata(const QString &track_id) {
|
||||
|
||||
if (!authenticated()) {
|
||||
Q_EMIT MetadataFailure(track_id, tr("Not authenticated"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (track_id.isEmpty()) {
|
||||
Q_EMIT MetadataFailure(track_id, tr("No track ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(u"tracks/"_s + track_id, ParamList());
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, track_id]() {
|
||||
TrackMetadataReceived(reply, track_id);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void SpotifyMetadataRequest::TrackMetadataReceived(QNetworkReply *reply, const QString &track_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
JsonObjectResult result = ParseJsonObject(reply);
|
||||
if (result.error_code != JsonBaseRequest::ErrorCode::Success) {
|
||||
Error(result.error_message);
|
||||
Q_EMIT MetadataFailure(track_id, result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject &json_obj = result.json_object;
|
||||
|
||||
Song song;
|
||||
song.set_source(Song::Source::Spotify);
|
||||
|
||||
// Parse song ID and URI
|
||||
if (json_obj.contains("id"_L1)) {
|
||||
song.set_song_id(json_obj["id"_L1].toString());
|
||||
}
|
||||
if (json_obj.contains("uri"_L1)) {
|
||||
song.set_url(QUrl(json_obj["uri"_L1].toString()));
|
||||
}
|
||||
|
||||
// Parse basic track info
|
||||
if (json_obj.contains("name"_L1)) {
|
||||
song.set_title(json_obj["name"_L1].toString());
|
||||
}
|
||||
if (json_obj.contains("track_number"_L1)) {
|
||||
song.set_track(json_obj["track_number"_L1].toInt());
|
||||
}
|
||||
if (json_obj.contains("disc_number"_L1)) {
|
||||
song.set_disc(json_obj["disc_number"_L1].toInt());
|
||||
}
|
||||
if (json_obj.contains("duration_ms"_L1)) {
|
||||
song.set_length_nanosec(json_obj["duration_ms"_L1].toVariant().toLongLong() * kNsecPerMsec);
|
||||
}
|
||||
|
||||
// Extract artist info
|
||||
QString artist_id;
|
||||
if (json_obj.contains("artists"_L1) && json_obj["artists"_L1].isArray()) {
|
||||
const QJsonArray array_artists = json_obj["artists"_L1].toArray();
|
||||
if (!array_artists.isEmpty()) {
|
||||
const QJsonObject obj_artist = array_artists.first().toObject();
|
||||
if (obj_artist.contains("id"_L1)) {
|
||||
artist_id = obj_artist["id"_L1].toString();
|
||||
song.set_artist_id(artist_id);
|
||||
}
|
||||
if (obj_artist.contains("name"_L1)) {
|
||||
song.set_artist(obj_artist["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract album info
|
||||
if (json_obj.contains("album"_L1) && json_obj["album"_L1].isObject()) {
|
||||
QJsonObject obj_album = json_obj["album"_L1].toObject();
|
||||
if (obj_album.contains("id"_L1)) {
|
||||
song.set_album_id(obj_album["id"_L1].toString());
|
||||
}
|
||||
if (obj_album.contains("name"_L1)) {
|
||||
song.set_album(obj_album["name"_L1].toString());
|
||||
}
|
||||
// Cover image - prefer larger images
|
||||
if (obj_album.contains("images"_L1) && obj_album["images"_L1].isArray()) {
|
||||
const QJsonArray array_images = obj_album["images"_L1].toArray();
|
||||
for (const QJsonValue &value : array_images) {
|
||||
if (!value.isObject()) continue;
|
||||
QJsonObject obj_image = value.toObject();
|
||||
if (!obj_image.contains("url"_L1) || !obj_image.contains("width"_L1) || !obj_image.contains("height"_L1)) continue;
|
||||
int width = obj_image["width"_L1].toInt();
|
||||
int height = obj_image["height"_L1].toInt();
|
||||
if (width >= 300 && height >= 300) {
|
||||
song.set_art_automatic(QUrl(obj_image["url"_L1].toString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Album artist
|
||||
if (obj_album.contains("artists"_L1) && obj_album["artists"_L1].isArray()) {
|
||||
const QJsonArray array_album_artists = obj_album["artists"_L1].toArray();
|
||||
if (!array_album_artists.isEmpty()) {
|
||||
const QJsonObject obj_album_artist = array_album_artists.first().toObject();
|
||||
if (obj_album_artist.contains("name"_L1)) {
|
||||
song.set_albumartist(obj_album_artist["name"_L1].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Release date
|
||||
if (obj_album.contains("release_date"_L1)) {
|
||||
QString release_date = obj_album["release_date"_L1].toString();
|
||||
if (release_date.length() >= 4) {
|
||||
song.set_year(release_date.left(4).toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
song.set_valid(true);
|
||||
|
||||
if (artist_id.isEmpty()) {
|
||||
// No artist ID - emit what we have without genre
|
||||
qLog(Debug) << "Spotify: Track metadata received for" << track_id << "(no artist ID for genre lookup)";
|
||||
Q_EMIT MetadataReceived(track_id, song);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store partial song and fetch artist metadata for genre
|
||||
pending_songs_[track_id] = song;
|
||||
|
||||
QNetworkReply *artist_reply = CreateRequest(u"artists/"_s + artist_id, ParamList());
|
||||
QObject::connect(artist_reply, &QNetworkReply::finished, this, [this, artist_reply, track_id]() {
|
||||
ArtistMetadataReceived(artist_reply, track_id);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void SpotifyMetadataRequest::ArtistMetadataReceived(QNetworkReply *reply, const QString &track_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
// Retrieve the stored partial song
|
||||
if (!pending_songs_.contains(track_id)) {
|
||||
Q_EMIT MetadataFailure(track_id, tr("No pending song for track ID"));
|
||||
return;
|
||||
}
|
||||
Song song = pending_songs_.take(track_id);
|
||||
|
||||
JsonObjectResult result = ParseJsonObject(reply);
|
||||
if (result.error_code != JsonBaseRequest::ErrorCode::Success) {
|
||||
// Still emit the song even without genre
|
||||
qLog(Warning) << "Spotify: Failed to get artist metadata for genre:" << result.error_message;
|
||||
Q_EMIT MetadataReceived(track_id, song);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject &json_object = result.json_object;
|
||||
|
||||
// Add genre from artist
|
||||
if (json_object.contains("genres"_L1) && json_object["genres"_L1].isArray()) {
|
||||
const QJsonArray array_genres = json_object["genres"_L1].toArray();
|
||||
if (!array_genres.isEmpty()) {
|
||||
song.set_genre(array_genres.first().toString());
|
||||
}
|
||||
}
|
||||
|
||||
qLog(Debug) << "Spotify: Track metadata received for" << track_id
|
||||
<< "- title:" << song.title()
|
||||
<< "- artist:" << song.artist()
|
||||
<< "- album:" << song.album()
|
||||
<< "- genre:" << song.genre();
|
||||
|
||||
Q_EMIT MetadataReceived(track_id, song);
|
||||
|
||||
}
|
||||
|
||||
void SpotifyMetadataRequest::Error(const QString &error_message, const QVariant &debug_output) {
|
||||
|
||||
qLog(Error) << "Spotify:" << error_message;
|
||||
if (debug_output.isValid()) qLog(Debug) << debug_output;
|
||||
|
||||
}
|
||||
58
src/spotify/spotifymetadatarequest.h
Normal file
58
src/spotify/spotifymetadatarequest.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYMETADATAREQUEST_H
|
||||
#define SPOTIFYMETADATAREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
#include "spotifybaserequest.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class SpotifyService;
|
||||
|
||||
class SpotifyMetadataRequest : public SpotifyBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpotifyMetadataRequest(SpotifyService *service, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
|
||||
void FetchTrackMetadata(const QString &track_id);
|
||||
|
||||
Q_SIGNALS:
|
||||
void MetadataReceived(QString track_id, Song song);
|
||||
void MetadataFailure(QString track_id, QString error);
|
||||
|
||||
private Q_SLOTS:
|
||||
void TrackMetadataReceived(QNetworkReply *reply, const QString &track_id);
|
||||
void ArtistMetadataReceived(QNetworkReply *reply, const QString &track_id);
|
||||
|
||||
private:
|
||||
void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
|
||||
QMap<QString, Song> pending_songs_; // track_id -> partial Song (waiting for artist genre)
|
||||
};
|
||||
|
||||
#endif // SPOTIFYMETADATAREQUEST_H
|
||||
Reference in New Issue
Block a user