Merge branch 'refactor/playqueue-split'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-31 11:19:50 +02:00
3 changed files with 262 additions and 215 deletions

View File

@@ -6,8 +6,9 @@ target_sources(qobuz-qt PRIVATE
mainwindow.hpp mainwindow.hpp
mainwindow.cpp mainwindow.cpp
# Queue (header-only) # Queue
playqueue.hpp playqueue.hpp
playqueue.cpp
# Backend (Qt wrapper around Rust FFI) # Backend (Qt wrapper around Rust FFI)
backend/qobuzbackend.hpp backend/qobuzbackend.hpp

238
src/playqueue.cpp Normal file
View File

@@ -0,0 +1,238 @@
#include "playqueue.hpp"
#include <algorithm>
#include <random>
PlayQueue::PlayQueue(QObject *parent) : QObject(parent) {}
void PlayQueue::setContext(const QJsonArray &tracks, int startIndex)
{
m_queue.clear();
m_playNext.clear();
// Only queue streamable tracks; find the filtered index for startIndex
int filteredStart = 0;
int filteredIdx = 0;
bool found = false;
for (int orig = 0; orig < tracks.size(); ++orig) {
const QJsonObject t = tracks[orig].toObject();
if (!t["streamable"].toBool(true))
continue;
if (!found && orig >= startIndex) {
filteredStart = filteredIdx;
found = true;
}
m_queue.append(t);
++filteredIdx;
}
m_index = qBound(0, filteredStart, qMax(0, m_queue.size() - 1));
if (m_shuffle)
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::reorderContext(const QJsonArray &tracks, qint64 currentId)
{
m_queue.clear();
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
m_index = 0;
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == currentId) {
m_index = i;
break;
}
}
emit queueChanged();
}
void PlayQueue::clearUpcoming()
{
m_playNext.clear();
if (m_index < m_queue.size())
m_queue.resize(m_index + 1); // keep up to and including current
emit queueChanged();
}
void PlayQueue::removeUpcoming(int upcomingIndex)
{
if (upcomingIndex < m_playNext.size()) {
m_playNext.removeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx < m_queue.size())
m_queue.removeAt(queueIdx);
}
emit queueChanged();
}
void PlayQueue::setShuffle(bool enabled)
{
if (m_shuffle == enabled) return;
m_shuffle = enabled;
if (enabled && !m_queue.isEmpty())
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::shuffleNow()
{
if (m_queue.isEmpty()) return;
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::addToQueue(const QJsonObject &track)
{
m_playNext.append(track);
emit queueChanged();
}
void PlayQueue::playNext(const QJsonObject &track)
{
m_playNext.prepend(track);
emit queueChanged();
}
bool PlayQueue::hasCurrent() const
{
return (!m_playNext.isEmpty()) || (!m_queue.isEmpty());
}
QJsonObject PlayQueue::current() const
{
if (!m_playNext.isEmpty()) return m_playNext.first();
if (m_index < m_queue.size()) return m_queue.at(m_index);
return {};
}
qint64 PlayQueue::currentId() const
{
return static_cast<qint64>(current()["id"].toDouble());
}
QJsonObject PlayQueue::advance()
{
if (!m_playNext.isEmpty()) {
// Return the playNext item directly — do NOT call current() after
// removal, as that would fall back to the already-playing m_index track.
const QJsonObject next = m_playNext.takeFirst();
emit queueChanged();
return next;
}
++m_index;
emit queueChanged();
return current();
}
QJsonObject PlayQueue::stepBack()
{
if (m_index > 0) --m_index;
emit queueChanged();
return current();
}
bool PlayQueue::canGoNext() const
{
return !m_playNext.isEmpty() || (m_index + 1 < m_queue.size());
}
void PlayQueue::setCurrentById(qint64 id)
{
m_playNext.clear();
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == id) {
m_index = i;
emit queueChanged();
return;
}
}
}
QVector<QJsonObject> PlayQueue::upcomingTracks(int maxCount) const
{
QVector<QJsonObject> result;
result.append(m_playNext);
for (int i = m_index + 1; i < m_queue.size() && result.size() < maxCount; ++i)
result.append(m_queue.at(i));
return result;
}
QJsonObject PlayQueue::skipToUpcoming(int upcomingIndex)
{
// Remove items 0..upcomingIndex-1 from the front of upcoming
for (int i = 0; i < upcomingIndex; ++i) {
if (!m_playNext.isEmpty())
m_playNext.removeFirst();
else if (m_index + 1 < m_queue.size())
++m_index;
}
// Pop and return the target (now at upcoming[0])
if (!m_playNext.isEmpty()) {
const QJsonObject t = m_playNext.takeFirst();
emit queueChanged();
return t;
}
if (m_index + 1 < m_queue.size()) {
++m_index;
emit queueChanged();
return m_queue.at(m_index);
}
emit queueChanged();
return {};
}
void PlayQueue::setUpcomingOrder(const QVector<QJsonObject> &newOrder)
{
m_playNext = newOrder;
m_queue.resize(m_index + 1); // drop old main-queue tail
emit queueChanged();
}
void PlayQueue::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();
}
void PlayQueue::moveUpcomingToTop(int upcomingIndex)
{
if (upcomingIndex < 0) return;
QJsonObject track;
if (upcomingIndex < m_playNext.size()) {
if (upcomingIndex == 0) return; // already at top
track = m_playNext.takeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx >= m_queue.size()) return;
track = m_queue.takeAt(queueIdx);
}
m_playNext.prepend(track);
emit queueChanged();
}
void PlayQueue::shuffleQueue(int keepAtFront)
{
if (m_queue.isEmpty()) return;
// Keep the current track at index 0 of the remaining queue
if (keepAtFront >= 0 && keepAtFront < m_queue.size()) {
QJsonObject current = m_queue.takeAt(keepAtFront);
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
m_queue.prepend(current);
} else {
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
}
m_index = 0;
}

View File

@@ -4,8 +4,6 @@
#include <QVector> #include <QVector>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <algorithm>
#include <random>
/// Local playback queue. Holds the ordered list of tracks for the current /// Local playback queue. Holds the ordered list of tracks for the current
/// context (album / playlist / search result / favourites) plus a separate /// context (album / playlist / search result / favourites) plus a separate
@@ -15,259 +13,83 @@ class PlayQueue : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit PlayQueue(QObject *parent = nullptr) : QObject(parent) {} explicit PlayQueue(QObject *parent = nullptr);
// ---- Loading a new context ---- // ---- Loading a new context ----
/// Replace the queue with all tracks from an album/playlist JSON context. /// Replace the queue with all tracks from an album/playlist JSON context.
/// @param startIndex Index of the track to start playing (-1 = first). /// @param startIndex Index of the track to start playing (-1 = first).
void setContext(const QJsonArray &tracks, int startIndex = 0) void setContext(const QJsonArray &tracks, int startIndex = 0);
{
m_queue.clear();
m_playNext.clear();
// Only queue streamable tracks; find the filtered index for startIndex
int filteredStart = 0;
int filteredIdx = 0;
bool found = false;
for (int orig = 0; orig < tracks.size(); ++orig) {
const QJsonObject t = tracks[orig].toObject();
if (!t["streamable"].toBool(true))
continue;
if (!found && orig >= startIndex) {
filteredStart = filteredIdx;
found = true;
}
m_queue.append(t);
++filteredIdx;
}
m_index = qBound(0, filteredStart, qMax(0, m_queue.size() - 1));
if (m_shuffle)
shuffleQueue(m_index);
emit queueChanged();
}
// ---- Re-order after a sort (keeps m_playNext, updates m_index) ---- // ---- Re-order after a sort (keeps m_playNext, updates m_index) ----
void reorderContext(const QJsonArray &tracks, qint64 currentId) void reorderContext(const QJsonArray &tracks, qint64 currentId);
{
m_queue.clear();
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
m_index = 0;
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == currentId) {
m_index = i;
break;
}
}
emit queueChanged();
}
// ---- Clear / remove upcoming ---- // ---- Clear / remove upcoming ----
/// Remove all "up next" entries (playNext + remaining main queue after current). /// Remove all "up next" entries (playNext + remaining main queue after current).
void clearUpcoming() void clearUpcoming();
{
m_playNext.clear();
if (m_index < m_queue.size())
m_queue.resize(m_index + 1); // keep up to and including current
emit queueChanged();
}
/// Remove one upcoming track by its index in upcomingTracks(). /// Remove one upcoming track by its index in upcomingTracks().
void removeUpcoming(int upcomingIndex) void removeUpcoming(int upcomingIndex);
{
if (upcomingIndex < m_playNext.size()) {
m_playNext.removeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx < m_queue.size())
m_queue.removeAt(queueIdx);
}
emit queueChanged();
}
// ---- Shuffle ---- // ---- Shuffle ----
bool shuffleEnabled() const { return m_shuffle; } bool shuffleEnabled() const { return m_shuffle; }
void setShuffle(bool enabled) void setShuffle(bool enabled);
{
if (m_shuffle == enabled) return;
m_shuffle = enabled;
if (enabled && !m_queue.isEmpty())
shuffleQueue(m_index);
emit queueChanged();
}
/// Shuffle the current queue once without changing the global shuffle flag. /// Shuffle the current queue once without changing the global shuffle flag.
void shuffleNow() void shuffleNow();
{
if (m_queue.isEmpty()) return;
shuffleQueue(m_index);
emit queueChanged();
}
// ---- Play-next prepend queue (like "Add to queue" ---- // ---- Play-next prepend queue (like "Add to queue" ----
void addToQueue(const QJsonObject &track) void addToQueue(const QJsonObject &track);
{
m_playNext.append(track);
emit queueChanged();
}
void playNext(const QJsonObject &track) void playNext(const QJsonObject &track);
{
m_playNext.prepend(track);
emit queueChanged();
}
// ---- Navigation ---- // ---- Navigation ----
bool hasCurrent() const bool hasCurrent() const;
{
return (!m_playNext.isEmpty()) || (!m_queue.isEmpty());
}
QJsonObject current() const QJsonObject current() const;
{
if (!m_playNext.isEmpty()) return m_playNext.first();
if (m_index < m_queue.size()) return m_queue.at(m_index);
return {};
}
qint64 currentId() const qint64 currentId() const;
{
return static_cast<qint64>(current()["id"].toDouble());
}
/// Advance and return the track to play next. Returns {} at end of queue. /// Advance and return the track to play next. Returns {} at end of queue.
QJsonObject advance() QJsonObject advance();
{
if (!m_playNext.isEmpty()) {
// Return the playNext item directly — do NOT call current() after
// removal, as that would fall back to the already-playing m_index track.
const QJsonObject next = m_playNext.takeFirst();
emit queueChanged();
return next;
}
++m_index;
emit queueChanged();
return current();
}
/// Step backwards in the main queue (play-next is not affected). /// Step backwards in the main queue (play-next is not affected).
QJsonObject stepBack() QJsonObject stepBack();
{
if (m_index > 0) --m_index;
emit queueChanged();
return current();
}
bool canGoNext() const bool canGoNext() const;
{
return !m_playNext.isEmpty() || (m_index + 1 < m_queue.size());
}
bool canGoPrev() const { return m_index > 0; } bool canGoPrev() const { return m_index > 0; }
// ---- Index lookup ---- // ---- Index lookup ----
/// Set the current position by track id (after user double-clicks a row). /// Set the current position by track id (after user double-clicks a row).
void setCurrentById(qint64 id) void setCurrentById(qint64 id);
{
m_playNext.clear();
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == id) {
m_index = i;
emit queueChanged();
return;
}
}
}
// ---- Accessors for queue panel ---- // ---- Accessors for queue panel ----
QVector<QJsonObject> upcomingTracks(int maxCount = 200) const QVector<QJsonObject> upcomingTracks(int maxCount = 200) const;
{
QVector<QJsonObject> result;
result.append(m_playNext);
for (int i = m_index + 1; i < m_queue.size() && result.size() < maxCount; ++i)
result.append(m_queue.at(i));
return result;
}
int playNextCount() const { return m_playNext.size(); } int playNextCount() const { return m_playNext.size(); }
int totalSize() const { return m_playNext.size() + m_queue.size(); } int totalSize() const { return m_playNext.size() + m_queue.size(); }
int currentIndex() const { return m_index; } int currentIndex() const { return m_index; }
/// Skip to upcoming[upcomingIndex]: removes everything before it, pops and returns it. /// Skip to upcoming[upcomingIndex]: removes everything before it, pops and returns it.
QJsonObject skipToUpcoming(int upcomingIndex) QJsonObject skipToUpcoming(int upcomingIndex);
{
// Remove items 0..upcomingIndex-1 from the front of upcoming
for (int i = 0; i < upcomingIndex; ++i) {
if (!m_playNext.isEmpty())
m_playNext.removeFirst();
else if (m_index + 1 < m_queue.size())
++m_index;
}
// Pop and return the target (now at upcoming[0])
if (!m_playNext.isEmpty()) {
const QJsonObject t = m_playNext.takeFirst();
emit queueChanged();
return t;
}
if (m_index + 1 < m_queue.size()) {
++m_index;
emit queueChanged();
return m_queue.at(m_index);
}
emit queueChanged();
return {};
}
/// Replace the upcoming list with a new order (used after drag-reorder in UI). /// Replace the upcoming list with a new order (used after drag-reorder in UI).
void setUpcomingOrder(const QVector<QJsonObject> &newOrder) void setUpcomingOrder(const QVector<QJsonObject> &newOrder);
{
m_playNext = newOrder;
m_queue.resize(m_index + 1); // drop old main-queue tail
emit queueChanged();
}
/// Append tracks to the main queue tail (autoplay/discovery). /// Append tracks to the main queue tail (autoplay/discovery).
void appendToContext(const QJsonArray &tracks) 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. /// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext.
void moveUpcomingToTop(int upcomingIndex) void moveUpcomingToTop(int upcomingIndex);
{
if (upcomingIndex < 0) return;
QJsonObject track;
if (upcomingIndex < m_playNext.size()) {
if (upcomingIndex == 0) return; // already at top
track = m_playNext.takeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx >= m_queue.size()) return;
track = m_queue.takeAt(queueIdx);
}
m_playNext.prepend(track);
emit queueChanged();
}
signals: signals:
void queueChanged(); void queueChanged();
@@ -278,19 +100,5 @@ private:
int m_index = 0; int m_index = 0;
bool m_shuffle = false; bool m_shuffle = false;
void shuffleQueue(int keepAtFront) void shuffleQueue(int keepAtFront);
{
if (m_queue.isEmpty()) return;
// Keep the current track at index 0 of the remaining queue
if (keepAtFront >= 0 && keepAtFront < m_queue.size()) {
QJsonObject current = m_queue.takeAt(keepAtFront);
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
m_queue.prepend(current);
} else {
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
}
m_index = 0;
}
}; };