feat(qt): mark tracks Qobuz pulled, and keep them out of the queue
Qobuz withdraws tracks: sometimes a recording, sometimes only a quality tier, sometimes it republishes the same recording under a new album id. The row keeps appearing and simply cannot play. Until now the user found out by clicking it. The app already knew and threw the answer away: eight builders hardcoded `streamable: true`, so the API's own flag never reached a row model. The reactive half was already here and stays -- a terminal failure still toasts and skips, bounded at five consecutive, with the 403 back-off kept separate so a 403 storm cannot burn five good tracks. Detection has two clauses. The API's `streamable: false`, and a session-only memory of tracks that died terminally under the player this run: /album/get is honest, but plenty of rows only fail at the stream-url step, and that memory is what stops the queue re-offering them. Session-only on purpose -- the reference kept the equivalent set in localStorage, never expired it and never revalidated it, so a track Qobuz restored stayed dead forever. Dead rows are dimmed, get an alert glyph and a tooltip, lose click, drag, favourite and download, and STAY visible and right-clickable: hiding them would defeat the point and remove the only route to a replacement. Qobuz's own player does exactly this, and the pulled track carries displayable:false while still being rendered -- that flag is not a hide instruction. A downloaded copy is exempt everywhere. The offline tier resolves before any availability question, so a pulled track already on disk plays fine; dropping it from the queue would take away something the user downloaded, and it would bite hardest offline. Those rows say they are playing the local copy and keep their play affordance. Two seams filter, not one, and both had to learn the surviving anchor: every caller read `first_id` from the UNFILTERED list, so the seam dropped that row and the app then asked the player for the id it had just discarded -- silence, no toast. That was already reachable today through the artist blacklist. The seams now return the surviving start and its id. Two id bugs surfaced on the way and are fixed here. A playlist row's `playlist_track_id` was filled with the CATALOG id; they are different numbers for the same row, one naming the recording and the other its slot in that playlist, and Qobuz's delete and reposition verbs both take the membership id -- so "remove from playlist" was addressing a row that does not exist. And the For You rows were built with `..default()`, claiming availability while the queue built from the same track read the truth. Catalogs: the two strings this needed already existed in all seven translated locales and are reused; the skipped-tracks toast is a plural pair, and the replacement feature's strings ride along here since they share the files. cc
This commit is contained in:
25 files changed
+1666
-161
No files matched your search
@@ -320,6 +320,22 @@ Rectangle {
|
||||
property bool reorderable: false
|
||||
|
||||
readonly property bool hovered: qrArea.containsMouse || menuArea.containsMouse
|
||||
/// Pulled from the catalogue, and not playable from disk. D5 keeps
|
||||
/// these out of a queue BUILD, so the only way one reaches this list is
|
||||
/// a session restored from disk or a listing whose endpoint never
|
||||
/// reported availability — the two cases the reactive backstop exists
|
||||
/// for. Rare is not never, and an unmarked dead row in the queue is the
|
||||
/// same lie D4 forbids everywhere else.
|
||||
///
|
||||
/// `cacheStatus` is read the same way rows/TrackRow.qml reads it and
|
||||
/// for the same reason (a pulled track the user DOWNLOADED still plays
|
||||
/// — contract §5.3/F5). `queue_qt::QueueRow` does not carry the field
|
||||
/// today, so it reads `undefined !== 3` = true and a downloaded-but-
|
||||
/// pulled queue row would be dimmed; the cost of that is cosmetic
|
||||
/// (nothing here gates playback on it) and it corrects itself the day
|
||||
/// the producer carries it.
|
||||
readonly property bool pulledDead: row.qobuzUnavailable === true
|
||||
&& row.cacheStatus !== 3
|
||||
readonly property bool isActive: inQueue && QbzPlayer.npTrackId !== "" && QbzPlayer.npTrackId === row.id
|
||||
readonly property bool dragSource: reorderable && root.reorderActive
|
||||
&& root.reorderFrom === rowIndex
|
||||
@@ -333,11 +349,14 @@ Rectangle {
|
||||
|
||||
height: 44
|
||||
radius: theme.radiusSm
|
||||
color: hovered ? theme.surfaceHover
|
||||
color: (hovered && !qrRoot.pulledDead) ? theme.surfaceHover
|
||||
: (rowIndex % 2 === 1 ? "#592a2a2a" : "transparent")
|
||||
border.width: dragSource ? 1 : 0
|
||||
border.color: theme.accent
|
||||
opacity: dragSource ? 0.4 : 1.0
|
||||
// The dead-row dim rides the SAME channel as the drag-source dim so the
|
||||
// two cannot fight; a dragged dead row is dimmer still, which is
|
||||
// harmless and honest.
|
||||
opacity: dragSource ? 0.4 : (qrRoot.pulledDead ? 0.5 : 1.0)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -358,7 +377,7 @@ Rectangle {
|
||||
readonly property bool marked: qrRoot.inQueue
|
||||
&& root.stopAfterId !== "" && row.id === root.stopAfterId
|
||||
Text {
|
||||
visible: !parent.marked
|
||||
visible: !parent.marked && !qrRoot.pulledDead
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
text: rowIndex + 1 + (doc.page || 0) * 40
|
||||
@@ -374,6 +393,25 @@ Rectangle {
|
||||
height: 13
|
||||
tintName: "accent"
|
||||
}
|
||||
// Same glyph and the same reason as rows/TrackRow.qml: the
|
||||
// alert takes the number's slot so the list does not reflow,
|
||||
// and it is `circle-alert` rather than `triangle-alert` to stay
|
||||
// clear of the cache-failed meaning that row's download column
|
||||
// gives the triangle.
|
||||
//
|
||||
// The stop-after marker wins the slot when both apply — it is
|
||||
// a marker the USER placed on this exact row and removing it
|
||||
// silently would be worse than leaving one dead row unglyphed
|
||||
// (the dim still marks it, and D5 means the pair is close to
|
||||
// unreachable anyway).
|
||||
QbzIcon {
|
||||
visible: qrRoot.pulledDead && !parent.marked
|
||||
anchors.centerIn: parent
|
||||
name: "circle-alert"
|
||||
width: 13
|
||||
height: 13
|
||||
tintName: "favorite"
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
visible: !showNumber
|
||||
@@ -563,7 +601,9 @@ Rectangle {
|
||||
// content) as rows/TrackRow.qml's trArea.
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// Arrow on a dead row, for the reason rows/TrackRow.qml gives: the
|
||||
// pointer must not promise a play the click below refuses.
|
||||
cursorShape: qrRoot.pulledDead ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
// THE reason drag-reorder scrolled instead of reordering: the
|
||||
// enclosing Flickable filters its children's mouse events and
|
||||
// grabs the press once the pointer moves past
|
||||
@@ -626,6 +666,13 @@ Rectangle {
|
||||
qrArea.suppressClick = false
|
||||
return
|
||||
}
|
||||
// D4 again: a dead row is inert on click. The ⋯ menu is
|
||||
// untouched, so "Remove from queue" — the one thing the user
|
||||
// actually wants to do with this row — still works, and the
|
||||
// press-drag reorder above still works too (it commits before
|
||||
// this handler ever runs).
|
||||
if (qrRoot.pulledDead)
|
||||
return
|
||||
if (inQueue) QbzQueue.queuePlayUpcoming(rowIndex)
|
||||
else QbzQueue.queuePlayHistory(rowIndex)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user