feat: qbz-1 streaming, gapless prefetch, accurate scrobbling, Range-seek
Port proven playback architecture from qbqt fork: - Bounded VecDeque buffer with condvar backpressure (4MB cap) - decrypt_and_extract_frames for clean FLAC frame extraction from ISOBMFF - Cancel+restart seeking with sub-segment sample skipping - start_prefetch / QueueNext for gapless transitions with pre-started downloads - track_transitioned signaling for scrobbler during gapless playback - Range-request HTTP seeking for non-segmented (MP3) tracks - OnceLock HTTP client singleton with cancel-aware chunked downloads - Accumulated listening time scrobbling (prevents false scrobbles from seeking) - Array-format Last.fm scrobble params (artist[0], track[0], etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,9 +78,11 @@ public slots:
|
||||
m_artist = track["album"].toObject()["artist"].toObject()["name"].toString();
|
||||
m_album = track["album"].toObject()["title"].toString();
|
||||
m_duration = static_cast<qint64>(track["duration"].toDouble());
|
||||
m_startTime = QDateTime::currentSecsSinceEpoch();
|
||||
m_playedSecs = 0;
|
||||
m_scrobbled = false;
|
||||
m_startTime = QDateTime::currentSecsSinceEpoch();
|
||||
m_playedSecs = 0;
|
||||
m_lastPosition = 0;
|
||||
m_accumulatedSecs = 0;
|
||||
m_scrobbled = false;
|
||||
|
||||
if (!isEnabled() || m_title.isEmpty() || m_duration < 30) return;
|
||||
updateNowPlaying();
|
||||
@@ -88,18 +90,29 @@ public slots:
|
||||
|
||||
void onPositionChanged(quint64 positionSecs, quint64 /*duration*/)
|
||||
{
|
||||
// Accumulate actual listening time to prevent false scrobbles from seeking
|
||||
if (positionSecs > m_lastPosition && (positionSecs - m_lastPosition) <= 2) {
|
||||
m_accumulatedSecs += (positionSecs - m_lastPosition);
|
||||
}
|
||||
m_lastPosition = positionSecs;
|
||||
m_playedSecs = positionSecs;
|
||||
|
||||
if (!isEnabled() || m_scrobbled || m_title.isEmpty() || m_duration < 30) return;
|
||||
|
||||
// Scrobble after 50% or 240 seconds played, whichever comes first, min 30 seconds.
|
||||
const quint64 threshold = static_cast<quint64>(qMin((qint64)240, m_duration / 2));
|
||||
if (positionSecs >= 30 && positionSecs >= threshold)
|
||||
if (m_accumulatedSecs >= 30 && m_accumulatedSecs >= threshold)
|
||||
scrobble();
|
||||
}
|
||||
|
||||
void onTrackFinished()
|
||||
{
|
||||
if (!isEnabled() || m_scrobbled || m_title.isEmpty() || m_duration < 30) return;
|
||||
if (m_playedSecs >= 30) scrobble();
|
||||
|
||||
const quint64 threshold = static_cast<quint64>(qMin((qint64)240, m_duration / 2));
|
||||
if (m_accumulatedSecs >= 30 && m_accumulatedSecs >= threshold) {
|
||||
scrobble();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -108,10 +121,12 @@ private:
|
||||
QString m_title;
|
||||
QString m_artist;
|
||||
QString m_album;
|
||||
qint64 m_duration = 0;
|
||||
qint64 m_startTime = 0;
|
||||
quint64 m_playedSecs = 0;
|
||||
bool m_scrobbled = false;
|
||||
qint64 m_duration = 0;
|
||||
qint64 m_startTime = 0;
|
||||
quint64 m_playedSecs = 0;
|
||||
quint64 m_lastPosition = 0;
|
||||
quint64 m_accumulatedSecs = 0;
|
||||
bool m_scrobbled = false;
|
||||
|
||||
void updateNowPlaying()
|
||||
{
|
||||
@@ -134,16 +149,18 @@ private:
|
||||
{
|
||||
m_scrobbled = true;
|
||||
QMap<QString,QString> params;
|
||||
params["method"] = QStringLiteral("track.scrobble");
|
||||
params["api_key"] = AppSettings::instance().lastFmApiKey();
|
||||
params["sk"] = AppSettings::instance().lastFmSessionKey();
|
||||
params["artist"] = m_artist;
|
||||
params["track"] = m_title;
|
||||
params["album"] = m_album;
|
||||
params["timestamp"] = QString::number(m_startTime);
|
||||
params["duration"] = QString::number(m_duration);
|
||||
params["api_sig"] = buildSig(params, AppSettings::instance().lastFmApiSecret());
|
||||
params["format"] = QStringLiteral("json");
|
||||
params["method"] = QStringLiteral("track.scrobble");
|
||||
params["api_key"] = AppSettings::instance().lastFmApiKey();
|
||||
params["sk"] = AppSettings::instance().lastFmSessionKey();
|
||||
|
||||
params["artist[0]"] = m_artist;
|
||||
params["track[0]"] = m_title;
|
||||
params["album[0]"] = m_album;
|
||||
params["timestamp[0]"] = QString::number(m_startTime);
|
||||
params["duration[0]"] = QString::number(m_duration);
|
||||
|
||||
params["api_sig"] = buildSig(params, AppSettings::instance().lastFmApiSecret());
|
||||
params["format"] = QStringLiteral("json");
|
||||
|
||||
auto *reply = m_nam->post(apiRequest(), encodeBody(params));
|
||||
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||
|
||||
Reference in New Issue
Block a user