feat: add autoplay suggestions when queue runs out
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
This commit is contained in:
@@ -56,6 +56,17 @@ void QobuzBackend::mostPopularSearch(const QString &query, quint32 limit)
|
||||
qobuz_backend_most_popular_search(m_backend, query.toUtf8().constData(), limit);
|
||||
}
|
||||
|
||||
void QobuzBackend::getDynamicSuggestions(const QJsonArray &listenedTrackIds, const QJsonArray &tracksToAnalyze, quint32 limit)
|
||||
{
|
||||
const QByteArray listened = QJsonDocument(listenedTrackIds).toJson(QJsonDocument::Compact);
|
||||
const QByteArray analyze = QJsonDocument(tracksToAnalyze).toJson(QJsonDocument::Compact);
|
||||
qobuz_backend_get_dynamic_suggestions(
|
||||
m_backend,
|
||||
listened.constData(),
|
||||
analyze.constData(),
|
||||
limit);
|
||||
}
|
||||
|
||||
void QobuzBackend::getAlbum(const QString &albumId)
|
||||
{
|
||||
qobuz_backend_get_album(m_backend, albumId.toUtf8().constData());
|
||||
@@ -282,6 +293,9 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
||||
case 25: // EV_DEEP_SHUFFLE_OK
|
||||
emit deepShuffleTracksLoaded(obj["tracks"].toArray());
|
||||
break;
|
||||
case 29: // EV_DYNAMIC_SUGGEST_OK
|
||||
emit dynamicSuggestionsLoaded(obj);
|
||||
break;
|
||||
case 27: // EV_GENRES_OK
|
||||
emit genresLoaded(obj);
|
||||
break;
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
// --- catalog ---
|
||||
void search(const QString &query, quint32 offset = 0, quint32 limit = 20);
|
||||
void mostPopularSearch(const QString &query, quint32 limit = 8);
|
||||
void getDynamicSuggestions(const QJsonArray &listenedTrackIds, const QJsonArray &tracksToAnalyze, quint32 limit = 50);
|
||||
void getAlbum(const QString &albumId);
|
||||
void getArtist(qint64 artistId);
|
||||
void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0);
|
||||
@@ -89,6 +90,7 @@ signals:
|
||||
void artistLoaded(const QJsonObject &artist);
|
||||
void artistReleasesLoaded(const QString &releaseType, const QJsonArray &items, bool hasMore, int offset);
|
||||
void deepShuffleTracksLoaded(const QJsonArray &tracks);
|
||||
void dynamicSuggestionsLoaded(const QJsonObject &result);
|
||||
void genresLoaded(const QJsonObject &result);
|
||||
void featuredAlbumsLoaded(const QJsonObject &result);
|
||||
void playlistLoaded(const QJsonObject &playlist);
|
||||
|
||||
@@ -45,6 +45,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
m_gapless->setChecked(AppSettings::instance().gaplessEnabled());
|
||||
playLayout->addRow(m_gapless);
|
||||
|
||||
m_autoplay = new QCheckBox(tr("Autoplay recommendations when queue ends"), playGroup);
|
||||
m_autoplay->setChecked(AppSettings::instance().autoplayEnabled());
|
||||
playLayout->addRow(m_autoplay);
|
||||
|
||||
layout->addWidget(playGroup);
|
||||
|
||||
// --- Last.fm group ---
|
||||
@@ -101,6 +105,7 @@ void SettingsDialog::applyChanges()
|
||||
AppSettings::instance().setPreferredFormat(m_formatBox->currentData().toInt());
|
||||
AppSettings::instance().setReplayGainEnabled(m_replayGain->isChecked());
|
||||
AppSettings::instance().setGaplessEnabled(m_gapless->isChecked());
|
||||
AppSettings::instance().setAutoplayEnabled(m_autoplay->isChecked());
|
||||
AppSettings::instance().setLastFmEnabled(m_lastFmEnabled->isChecked());
|
||||
AppSettings::instance().setLastFmApiKey(m_lastFmApiKey->text().trimmed());
|
||||
AppSettings::instance().setLastFmApiSecret(m_lastFmApiSecret->text().trimmed());
|
||||
|
||||
@@ -20,6 +20,7 @@ private:
|
||||
QComboBox *m_formatBox = nullptr;
|
||||
QCheckBox *m_replayGain = nullptr;
|
||||
QCheckBox *m_gapless = nullptr;
|
||||
QCheckBox *m_autoplay = nullptr;
|
||||
|
||||
// Last.fm
|
||||
QCheckBox *m_lastFmEnabled = nullptr;
|
||||
|
||||
@@ -241,6 +241,17 @@ public:
|
||||
emit queueChanged();
|
||||
}
|
||||
|
||||
/// Append tracks to the main queue tail (autoplay/discovery).
|
||||
void appendToContext(const QJsonArray &tracks)
|
||||
{
|
||||
for (const auto &v : tracks) {
|
||||
const QJsonObject t = v.toObject();
|
||||
if (t["streamable"].toBool(true))
|
||||
m_queue.append(t);
|
||||
}
|
||||
emit queueChanged();
|
||||
}
|
||||
|
||||
/// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext.
|
||||
void moveUpcomingToTop(int upcomingIndex)
|
||||
{
|
||||
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
bool gaplessEnabled() const { return m_settings.value("playback/gapless", false).toBool(); }
|
||||
void setGaplessEnabled(bool v) { m_settings.setValue("playback/gapless", v); }
|
||||
|
||||
bool autoplayEnabled() const { return m_settings.value("playback/autoplay", false).toBool(); }
|
||||
void setAutoplayEnabled(bool v) { m_settings.setValue("playback/autoplay", v); }
|
||||
|
||||
// --- Last.fm ---
|
||||
bool lastFmEnabled() const { return m_settings.value("lastfm/enabled", false).toBool(); }
|
||||
void setLastFmEnabled(bool v) { m_settings.setValue("lastfm/enabled", v); }
|
||||
|
||||
@@ -94,6 +94,11 @@ MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
||||
m_shuffle->setCheckable(true);
|
||||
connect(m_shuffle, &QAction::toggled, this, &MainToolBar::onShuffleToggled);
|
||||
|
||||
m_autoplay = addAction(Icon::repeat(), tr("Autoplay"));
|
||||
m_autoplay->setCheckable(true);
|
||||
m_autoplay->setChecked(AppSettings::instance().autoplayEnabled());
|
||||
connect(m_autoplay, &QAction::toggled, this, &MainToolBar::onAutoplayToggled);
|
||||
|
||||
// ---- Volume ----
|
||||
m_volume = new VolumeButton(this);
|
||||
addWidget(m_volume);
|
||||
@@ -118,6 +123,8 @@ MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
||||
connect(m_backend, &QobuzBackend::positionChanged, this, &MainToolBar::onPositionChanged);
|
||||
connect(m_backend, &QobuzBackend::trackFinished, this, &MainToolBar::onTrackFinished);
|
||||
connect(m_backend, &QobuzBackend::trackTransitioned, this, &MainToolBar::onTrackTransitioned);
|
||||
connect(m_backend, &QobuzBackend::dynamicSuggestionsLoaded,
|
||||
this, &MainToolBar::onDynamicSuggestionsLoaded);
|
||||
|
||||
// ---- Queue signals ----
|
||||
connect(m_queue, &PlayQueue::queueChanged, this, &MainToolBar::onQueueChanged);
|
||||
@@ -232,6 +239,12 @@ void MainToolBar::onBackendStateChanged(const QString &state)
|
||||
void MainToolBar::onTrackChanged(const QJsonObject &track)
|
||||
{
|
||||
setCurrentTrack(track);
|
||||
|
||||
if (track.contains("id")) {
|
||||
m_recentTracks.append(track);
|
||||
while (m_recentTracks.size() > 32)
|
||||
m_recentTracks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void MainToolBar::onPositionChanged(quint64 position, quint64 duration)
|
||||
@@ -243,6 +256,8 @@ void MainToolBar::onTrackFinished()
|
||||
{
|
||||
if (m_queue->canGoNext()) {
|
||||
onNext();
|
||||
} else if (AppSettings::instance().autoplayEnabled()) {
|
||||
requestAutoplaySuggestions();
|
||||
} else {
|
||||
setPlaying(false);
|
||||
m_progress->setValue(0);
|
||||
@@ -272,6 +287,81 @@ void MainToolBar::onShuffleToggled(bool checked)
|
||||
m_queue->setShuffle(checked);
|
||||
}
|
||||
|
||||
void MainToolBar::onAutoplayToggled(bool checked)
|
||||
{
|
||||
AppSettings::instance().setAutoplayEnabled(checked);
|
||||
}
|
||||
|
||||
void MainToolBar::requestAutoplaySuggestions()
|
||||
{
|
||||
if (m_fetchingAutoplay)
|
||||
return;
|
||||
|
||||
QJsonArray listenedIds;
|
||||
QJsonArray analyze;
|
||||
|
||||
const int n = m_recentTracks.size();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const QJsonObject t = m_recentTracks.at(i);
|
||||
const qint64 trackId = static_cast<qint64>(t["id"].toDouble());
|
||||
if (trackId <= 0)
|
||||
continue;
|
||||
|
||||
listenedIds.append(trackId);
|
||||
|
||||
if (i < qMax(0, n - 5))
|
||||
continue;
|
||||
|
||||
const qint64 artistId = static_cast<qint64>(
|
||||
t["performer"].toObject()["id"].toDouble());
|
||||
const qint64 genreId = static_cast<qint64>(
|
||||
t["album"].toObject()["genre"].toObject()["id"].toDouble());
|
||||
const qint64 labelId = static_cast<qint64>(
|
||||
t["album"].toObject()["label"].toObject()["id"].toDouble());
|
||||
|
||||
analyze.append(QJsonObject{
|
||||
{"track_id", trackId},
|
||||
{"artist_id", artistId},
|
||||
{"genre_id", genreId},
|
||||
{"label_id", labelId},
|
||||
});
|
||||
}
|
||||
|
||||
if (listenedIds.isEmpty() || analyze.isEmpty()) {
|
||||
setPlaying(false);
|
||||
m_progress->setValue(0);
|
||||
m_position->setText(QStringLiteral("0:00 / 0:00"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_fetchingAutoplay = true;
|
||||
m_backend->getDynamicSuggestions(listenedIds, analyze, 50);
|
||||
}
|
||||
|
||||
void MainToolBar::onDynamicSuggestionsLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_fetchingAutoplay = false;
|
||||
|
||||
QJsonArray items;
|
||||
if (result["tracks"].isObject())
|
||||
items = result["tracks"].toObject()["items"].toArray();
|
||||
if (items.isEmpty() && result["dynamic"].isObject())
|
||||
items = result["dynamic"].toObject()["items"].toArray();
|
||||
if (items.isEmpty())
|
||||
items = result["items"].toArray();
|
||||
|
||||
if (items.isEmpty()) {
|
||||
setPlaying(false);
|
||||
m_progress->setValue(0);
|
||||
m_position->setText(QStringLiteral("0:00 / 0:00"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_queue->appendToContext(items);
|
||||
if (m_queue->canGoNext())
|
||||
onNext();
|
||||
}
|
||||
|
||||
void MainToolBar::fetchAlbumArt(const QString &url)
|
||||
{
|
||||
m_nam->get(QNetworkRequest(QUrl(url)));
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QVector>
|
||||
|
||||
class MainToolBar : public QToolBar
|
||||
{
|
||||
@@ -47,6 +48,8 @@ private slots:
|
||||
void onTrackTransitioned();
|
||||
void onQueueChanged();
|
||||
void onShuffleToggled(bool checked);
|
||||
void onAutoplayToggled(bool checked);
|
||||
void onDynamicSuggestionsLoaded(const QJsonObject &result);
|
||||
|
||||
void fetchAlbumArt(const QString &url);
|
||||
void onAlbumArtReady(QNetworkReply *reply);
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
QLabel *m_position = nullptr;
|
||||
QWidget *m_rightSpacer = nullptr;
|
||||
QAction *m_shuffle = nullptr;
|
||||
QAction *m_autoplay = nullptr;
|
||||
VolumeButton *m_volume = nullptr;
|
||||
QAction *m_queueBtn = nullptr;
|
||||
QAction *m_search = nullptr;
|
||||
@@ -72,6 +76,10 @@ private:
|
||||
QNetworkAccessManager *m_nam = nullptr;
|
||||
QString m_currentArtUrl;
|
||||
QJsonObject m_currentTrack;
|
||||
QVector<QJsonObject> m_recentTracks;
|
||||
bool m_playing = false;
|
||||
bool m_seeking = false;
|
||||
bool m_fetchingAutoplay = false;
|
||||
|
||||
void requestAutoplaySuggestions();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user