fix(qt): treat every server source as non-catalog, not just Plex
Three places still spelled "not a Qobuz row" as "local or plex", which made a Jellyfin or Subsonic row fall into the catalog class: - Library ALL feed stamped the album source from the ID PREFIX (`plex:`), so every media-server album came out as "local" and drew the local hard-drive badge. Reads the row's own `source` word now — byte-identical for local and Plex, correct for the rest. The same failure `local_rows::badge_source` had. - TrackRow's `localSourceRow` gated "Add to playlist" and the offline download entry. A Jellyfin row counted as a catalog row, so the menu offered to send a namespaced synthetic id to the Qobuz playlist API, which adds an UNRELATED track. An allowlist, not `!== "qobuz"`: "offline" is an offline-CACHED Qobuz row whose id IS a catalog id. - AlbumCard hid "Block this album" on local/plex only, so a server album offered to blacklist an id the Qobuz catalog never had. `feed_track_to_queue` is taught the words too. Media-server TRACKS do not ride that feed today (only their albums do), so that one is the guard learning the vocabulary before it is needed rather than a live fix — its existing test, which pins that a local id must never resolve as the Qobuz track carrying the same number, now covers all of them. cc
This commit is contained in:
3 files changed
+77
-12
No files matched your search
@@ -55,8 +55,9 @@ Rectangle {
|
||||
property string artworkUrl: ""
|
||||
property bool isFavorite: false
|
||||
property bool isPinned: false
|
||||
// The row's SOURCE word — "local" | "offline" | "plex" (the Local Library
|
||||
// badge triple, src/local_rows.rs `badge_source`) or "qobuz" (the Library
|
||||
// The row's SOURCE word — "local" | "offline" | "plex" | "jellyfin" |
|
||||
// a Subsonic brand spelling (the Local Library badge set,
|
||||
// src/local_rows.rs `badge_source`) or "qobuz" (the Library
|
||||
// ALL feed, library_qt.rs `FeedItem.source`). "" = a catalog surface that
|
||||
// publishes no source at all.
|
||||
//
|
||||
@@ -65,6 +66,15 @@ Rectangle {
|
||||
// (AlbumCard.slint:434 keeps the entry off local/plex rows). Hosts that
|
||||
// want the badge hidden set `showSourceBadge`, never `source: ""`.
|
||||
property string source: ""
|
||||
/// Source words that mean "this album does not live in the Qobuz catalog".
|
||||
/// Same fold as `SourceIcon.isSubsonic`: a row stamped with a Subsonic
|
||||
/// BRAND is still a server row, so it must not be offered a catalog-only
|
||||
/// action.
|
||||
readonly property bool serverAlbum: root.source === "local"
|
||||
|| root.source === "plex" || root.source === "jellyfin"
|
||||
|| root.source === "subsonic" || root.source === "navidrome"
|
||||
|| root.source === "gonic" || root.source === "airsonic"
|
||||
|| root.source === "astiga"
|
||||
// discover/AlbumCard.slint:79 `show-source-badge` — default OFF; the two
|
||||
// hosts that turn it on are the Library ALL grid (gated on the toolbar's
|
||||
// show-local toggle, FavoritesView.slint:1097) and the Local Library
|
||||
@@ -256,8 +266,11 @@ Rectangle {
|
||||
if (root.catalogAffordances) {
|
||||
m.push({ "label": root.isFavorite ? t("Remove from Library", r) : t("Add to Library", r),
|
||||
"icon": root.isFavorite ? "heart-filled" : "heart", "action": "favorite" })
|
||||
// .slint gates this on a non-local/plex source as well.
|
||||
if (root.hasBlacklistSeam && root.source !== "local" && root.source !== "plex")
|
||||
// .slint gates this on a non-local/plex source as well — and the
|
||||
// rule is "not a Qobuz catalog album", so every server source is
|
||||
// in the same class as Plex. Blocking a Jellyfin album would write
|
||||
// a blacklist entry against an id the Qobuz catalog never had.
|
||||
if (root.hasBlacklistSeam && !root.serverAlbum)
|
||||
m.push({ "label": t("Block this album", r), "icon": "blind-eye", "action": "block" })
|
||||
}
|
||||
// Host tail (see `extraMenuEntries`). `concat` so the host's array is
|
||||
|
||||
@@ -228,9 +228,10 @@ Rectangle {
|
||||
// unmistakable (playlist_picker_qt.rs's four-case matrix):
|
||||
//
|
||||
// catalogRow `item.id` is a Qobuz catalog id -> openForTrack.
|
||||
// localSourceRow the row carries its own source word and it says
|
||||
// local/plex -> `item.id` is a library.db row id or
|
||||
// a Plex synthetic id. Sending it to the Qobuz arm
|
||||
// localSourceRow the row carries its own source word and it names
|
||||
// a NON-CATALOG origin -> `item.id` is a library.db
|
||||
// row id or a namespaced synthetic id (Plex,
|
||||
// Jellyfin, Subsonic). Sending it to the Qobuz arm
|
||||
// adds an UNRELATED track, so the entry is only
|
||||
// offered when the HOST takes the routing
|
||||
// (`routePlaylistAddExternally`, below), and is
|
||||
@@ -241,8 +242,17 @@ Rectangle {
|
||||
//
|
||||
// Ephemeral rows never reach this decision: their surface sets
|
||||
// `showMenu: false`, so the whole menu is absent (LocalEphemeralPane).
|
||||
readonly property bool localSourceRow:
|
||||
root.item.source === "local" || root.item.source === "plex"
|
||||
// An ALLOWLIST of non-catalog words, not `!== "qobuz"`: "offline" is an
|
||||
// offline-CACHED Qobuz row whose id IS a catalog id, so inverting the test
|
||||
// would strip Add-to-playlist from the rows that legitimately have it.
|
||||
// The Subsonic brand spellings fold here for the same reason they fold in
|
||||
// `SourceIcon.isSubsonic` — a row stamped "navidrome" is not a Qobuz row.
|
||||
readonly property bool localSourceRow: {
|
||||
var s = root.item.source
|
||||
return s === "local" || s === "plex" || s === "jellyfin"
|
||||
|| s === "subsonic" || s === "navidrome" || s === "gonic"
|
||||
|| s === "airsonic" || s === "astiga"
|
||||
}
|
||||
/// Hand "Add to playlist" to the HOST (the `routeGoToExternally` idiom):
|
||||
/// the host answers `playlistAddRequested()` with the LOCAL-mode call its
|
||||
/// surface can make. The local playlist detail is the one that needs it —
|
||||
|
||||
@@ -897,7 +897,24 @@ fn all_local_feed_blocking() -> Vec<FeedItem> {
|
||||
.unwrap_or_default();
|
||||
let n = albums.len();
|
||||
for (i, a) in albums.into_iter().enumerate() {
|
||||
let source = if a.id.starts_with("plex:") { "plex" } else { "local" };
|
||||
// The row's own `source` word, NOT the id prefix. Both answer "plex"
|
||||
// for a Plex album (group_key is `plex:<hash>` and the CTE stamps
|
||||
// 'plex'), so this is byte-identical for the two sources that existed
|
||||
// — but a media server's group_key is `jellyfin:<id>` / `navidrome:<id>`,
|
||||
// and the prefix test folded every one of them to "local". That is the
|
||||
// same failure `local_rows::badge_source` had: the ALL feed drew the
|
||||
// local hard-drive on albums that live on a server.
|
||||
//
|
||||
// "user" and "qobuz_download" keep answering "local" exactly as before;
|
||||
// this feed's `source` vocabulary is consumed by `feed_track_to_queue`
|
||||
// and by the QML badge, and widening it to "offline" here would be a
|
||||
// separate change with its own consumers to check.
|
||||
let source = match a.source.as_str() {
|
||||
"plex" => "plex",
|
||||
"jellyfin" => "jellyfin",
|
||||
"subsonic" | "navidrome" | "gonic" | "airsonic" | "astiga" => "subsonic",
|
||||
_ => "local",
|
||||
};
|
||||
out.push(
|
||||
FeedItem {
|
||||
kind: "album".into(),
|
||||
@@ -1608,6 +1625,22 @@ pub(crate) fn apply_pin_change(kind: &str, id: &str, pinned: bool) {
|
||||
// under the pointer — identical on the Tracks tab, and in the All feed it
|
||||
// queues the mixed feed's track rows in the order shown instead of nothing.
|
||||
|
||||
/// Every source word whose row id is NOT a Qobuz catalog id.
|
||||
///
|
||||
/// The list is an ALLOWLIST rather than `!= "qobuz"` because the fall-through
|
||||
/// below parses the id as a u64 and resolves it against the Qobuz catalog: a
|
||||
/// word this function has not been taught silently becomes "somebody else's
|
||||
/// track", which is exactly the failure the test below pins for local/plex.
|
||||
/// Media-server tracks do not ride this feed today (only their ALBUMS do), so
|
||||
/// this is the guard being taught the words BEFORE they arrive, not a fix for
|
||||
/// a live bug.
|
||||
fn is_local_source(source: &str) -> bool {
|
||||
matches!(
|
||||
source,
|
||||
"local" | "plex" | "jellyfin" | "subsonic" | "navidrome" | "gonic" | "airsonic" | "astiga"
|
||||
)
|
||||
}
|
||||
|
||||
/// `playback_qt::feed_queue_track` for a feed row already in hand.
|
||||
///
|
||||
/// Mirrors that function field-for-field (it looks the same row up by id and
|
||||
@@ -1632,7 +1665,7 @@ pub(crate) fn apply_pin_change(kind: &str, id: &str, pinned: bool) {
|
||||
/// Slint builds — its `FAV_CURRENT` is Qobuz-only — and it is strictly better
|
||||
/// than resolving it as somebody else's track.
|
||||
pub(crate) fn feed_track_to_queue(item: &FeedItem) -> Option<QueueTrack> {
|
||||
if item.source == "local" || item.source == "plex" {
|
||||
if is_local_source(&item.source) {
|
||||
let row_id = item.id.parse::<i64>().ok()?;
|
||||
let raw = LOCAL_RAW.lock().ok()?.get(&row_id).cloned();
|
||||
return match raw {
|
||||
@@ -1813,7 +1846,16 @@ mod tests {
|
||||
/// `QueueTrack` with `source: "qobuz"` and this id.
|
||||
#[test]
|
||||
fn a_local_row_is_never_resolved_as_a_qobuz_track() {
|
||||
for source in ["local", "plex"] {
|
||||
for source in [
|
||||
"local",
|
||||
"plex",
|
||||
"jellyfin",
|
||||
"subsonic",
|
||||
"navidrome",
|
||||
"gonic",
|
||||
"airsonic",
|
||||
"astiga",
|
||||
] {
|
||||
let mut item = track("42", "Ghost in the Machine");
|
||||
item.source = source.into();
|
||||
assert!(
|
||||
|
||||
Reference in new issue
Block a user