fix(source): a source word the caller supplied outranks a namespace bit
Four George Harrison albums would not play from a MyQBZ collection:
ambiguous id "5099951336752" (Some(Album)):
candidates [SourceId("qobuz"), SourceId("subsonic")];
caller must supply a source
The caller HAD supplied one. The MyQBZ row stores `source = 'qobuz'` and
`RawRef::from_mixtape_item` threads it through correctly; nothing about
the storage or the call chain was wrong. The bug is that
`SubsonicSource::recognises` ORed a namespace-bit test ON TOP of the
source word, so it claimed an id it can never own while Qobuz claimed it
legitimately — two hits, and `claim` refuses to guess.
`5099951336752` is an EAN-13 barcode (EMI/Parlophone, "Somewhere In
England"). Checked arithmetically rather than assumed: bit 42 set,
nothing above the payload mask — exactly `RemoteSource::of_id`'s Subsonic
predicate. The reserved band is 4_398_046_511_104 ..= 5_497_558_138_879,
i.e. EVERY 13-digit barcode from 4398… to 5497…, which is all of
EMI/Parlophone's 5099… prefix. That is why four albums failed together
and why it is deterministic per album rather than flaky.
Two gates, applied to all three cached sources:
- An explicit and DIFFERENT source word now returns false outright. A
bit pattern must never outvote a caller who already said what this
is.
- A namespaced numeric is a TRACK rowid by construction, so the arm no
longer fires in ALBUM position at all. That is the narrower and more
honest statement, and on its own it would have spared this barcode.
Plex needed the same edit and not as tidying: its arm is the loosest of
the three (a bare bit test, no payload bound) and its Album branch
answers `Err(BadIdShape)`, which the registry propagates AHEAD of any
tiebreak — so a barcode with bit 40 set failed with a confusing *Plex*
error on an id Plex never owned.
WHY THIS WAS NEVER CAUGHT: every disjointness test used the barcode
"0060254702523", whose LEADING ZERO puts it below all three floors, so
the entire in-band class was untested. The new test uses the real id and
asserts up front that it still sits in the band, so it cannot quietly
stop covering the case. Verified the honest way — with the old predicate
restored the new test FAILS, with the fix it passes; 90 tests green, and
`namespace_bit_is_enough_to_recognise_without_a_source_word` (the Plex
mixtape-track case that must not regress) stays green.
cc
This commit is contained in:
3 files changed
+155
-28
No files matched your search
@@ -115,17 +115,32 @@ impl Default for JellyfinSource {
|
||||
/// is guaranteed, and guessing a source from a string's alphabet is exactly the
|
||||
/// class of inference the seam exists to delete.
|
||||
pub(crate) fn recognises(raw: &RawRef) -> bool {
|
||||
raw.source == Some(SourceId::JELLYFIN)
|
||||
// The PREFIXED album key the Local Library grid publishes. Without
|
||||
// this arm an album card carrying no source word — which is every card
|
||||
// that round-trips through a QML string property — would be claimed by
|
||||
// nobody and the album page would open empty. Plex solves the same
|
||||
// problem the same way, with its `plex:` keys.
|
||||
|| raw.id.trim().starts_with("jellyfin:")
|
||||
|| raw
|
||||
.numeric()
|
||||
.map(|n| RemoteSource::of_id(n as i64) == Some(RemoteSource::Jellyfin))
|
||||
.unwrap_or(false)
|
||||
if raw.source == Some(SourceId::JELLYFIN) {
|
||||
return true;
|
||||
}
|
||||
// AN EXPLICIT, DIFFERENT SOURCE WORD IS EVIDENCE AGAINST OWNERSHIP — see
|
||||
// the long note in sources/subsonic.rs. A namespace bit must never
|
||||
// outvote a caller who already said which source this is.
|
||||
if raw.source.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PREFIXED album key the Local Library grid publishes. Without
|
||||
// this arm an album card carrying no source word — which is every card
|
||||
// that round-trips through a QML string property — would be claimed by
|
||||
// nobody and the album page would open empty.
|
||||
if raw.id.trim().starts_with("jellyfin:") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A namespaced numeric is a TRACK rowid by construction; a Jellyfin album
|
||||
// id is a 32-character hex GUID, never one of these.
|
||||
if raw.kind == Some(ItemKind::Album) {
|
||||
return false;
|
||||
}
|
||||
raw.numeric()
|
||||
.map(|n| RemoteSource::of_id(n as i64) == Some(RemoteSource::Jellyfin))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -635,4 +650,32 @@ mod tests {
|
||||
s.set_creds(Some(Arc::new(Off)));
|
||||
assert!(s.server().is_none());
|
||||
}
|
||||
|
||||
/// THE BARCODE THAT BROKE FOUR ALBUMS AT ONCE (2026-08-22).
|
||||
///
|
||||
/// `2500000000000` is a 13-digit barcode in Jellyfin's reserved band. It has
|
||||
/// bit 41 set and nothing above the payload, i.e. it satisfies the
|
||||
/// Subsonic namespace predicate exactly, so this source used to claim it
|
||||
/// alongside Qobuz and `claim` returned `Ambiguous`.
|
||||
///
|
||||
/// The reserved band is 2_199_023_255_552 ..= 3_298_534_883_327 — EVERY
|
||||
/// 13-digit barcode from 2199… to 3298…. The pre-existing disjointness tests all used
|
||||
/// "0060254702523", whose LEADING ZERO puts it below every floor, so this
|
||||
/// entire class was untested.
|
||||
#[test]
|
||||
fn an_ean13_barcode_in_the_namespace_band_is_not_ours() {
|
||||
let id = "2500000000000";
|
||||
assert_eq!(
|
||||
id.parse::<i64>().unwrap() & (1 << 41),
|
||||
1 << 41,
|
||||
"the barcode really does sit in the reserved band — if this ever \
|
||||
fails the test has stopped covering the case it was written for"
|
||||
);
|
||||
// Named by the caller: the source word must win outright.
|
||||
assert!(!recognises(&RawRef::new("qobuz", ItemKind::Album, id)));
|
||||
// Even unnamed, an ALBUM id is never a namespaced track rowid.
|
||||
let mut unnamed = RawRef::new("qobuz", ItemKind::Album, id);
|
||||
unnamed.source = None;
|
||||
assert!(!recognises(&unnamed));
|
||||
}
|
||||
}
|
||||
@@ -240,12 +240,32 @@ pub fn is_thumb_path(path: &str) -> bool {
|
||||
/// Split out of `claim` so the predicate is testable without touching
|
||||
/// `plex_cache.db` (the rating-key ladder's case 2 queries it).
|
||||
pub(crate) fn recognises(raw: &RawRef) -> bool {
|
||||
raw.source == Some(SourceId::PLEX)
|
||||
|| raw.id.trim().starts_with("plex:")
|
||||
|| raw
|
||||
.numeric()
|
||||
.map(|n| n & PLEX_TRACK_ID_FLOOR != 0)
|
||||
.unwrap_or(false)
|
||||
if raw.source == Some(SourceId::PLEX) {
|
||||
return true;
|
||||
}
|
||||
// AN EXPLICIT, DIFFERENT SOURCE WORD IS EVIDENCE AGAINST OWNERSHIP — see
|
||||
// the long note in sources/subsonic.rs. A namespace bit must never
|
||||
// outvote a caller who already said which source this is.
|
||||
if raw.source.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if raw.id.trim().starts_with("plex:") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A namespaced numeric is a TRACK rowid by construction. This arm is the
|
||||
// LOOSEST of the three (a bare bit test, no payload bound), so an album
|
||||
// barcode with bit 40 set used to land in `claim`'s Album branch and come
|
||||
// back as `Err(BadIdShape)` — which registry.rs propagates IMMEDIATELY,
|
||||
// ahead of any tiebreak. That failed the caller with a confusing *Plex*
|
||||
// error on an id Plex never owned.
|
||||
if raw.kind == Some(ItemKind::Album) {
|
||||
return false;
|
||||
}
|
||||
raw.numeric()
|
||||
.map(|n| n & PLEX_TRACK_ID_FLOOR != 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
||||
@@ -102,17 +102,51 @@ impl Default for SubsonicSource {
|
||||
/// POSITIVE ownership: the source word (in any of its brand spellings, which
|
||||
/// `SourceId::from_word` has already folded), or the namespace bit.
|
||||
pub(crate) fn recognises(raw: &RawRef) -> bool {
|
||||
raw.source == Some(SourceId::SUBSONIC)
|
||||
// The PREFIXED album key the Local Library grid publishes. Without
|
||||
// this arm an album card carrying no source word — which is every card
|
||||
// that round-trips through a QML string property — would be claimed by
|
||||
// nobody and the album page would open empty. Plex solves the same
|
||||
// problem the same way, with its `plex:` keys.
|
||||
|| raw.id.trim().starts_with("subsonic:")
|
||||
|| raw
|
||||
.numeric()
|
||||
.map(|n| RemoteSource::of_id(n as i64) == Some(RemoteSource::Subsonic))
|
||||
.unwrap_or(false)
|
||||
if raw.source == Some(SourceId::SUBSONIC) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AN EXPLICIT, DIFFERENT SOURCE WORD IS EVIDENCE AGAINST OWNERSHIP.
|
||||
//
|
||||
// Without this the namespace-bit arm below OUTVOTES the caller. Measured
|
||||
// 2026-08-22 from the owner's log: four George Harrison albums played
|
||||
// from a MyQBZ collection failed with
|
||||
// ambiguous id "5099951336752" (Some(Album)):
|
||||
// candidates [SourceId("qobuz"), SourceId("subsonic")]
|
||||
// `5099951336752` is an EAN-13 BARCODE. It happens to have bit 42 set and
|
||||
// nothing above the payload, which is exactly the Subsonic predicate, so
|
||||
// this source claimed an id it can never own while Qobuz claimed it
|
||||
// legitimately off the stored source word — two hits, and `claim` refuses
|
||||
// to guess.
|
||||
//
|
||||
// The collision is not a freak: the reserved band is
|
||||
// 4_398_046_511_104 ..= 5_497_558_138_879, which is EVERY 13-digit barcode
|
||||
// from 4398… to 5497… — all of EMI/Parlophone's 5099… prefix. That is why
|
||||
// four albums failed together and why it is deterministic, not flaky.
|
||||
if raw.source.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PREFIXED album key the Local Library grid publishes. Without
|
||||
// this arm an album card carrying no source word — which is every card
|
||||
// that round-trips through a QML string property — would be claimed by
|
||||
// nobody and the album page would open empty. Plex solves the same
|
||||
// problem the same way, with its `plex:` keys.
|
||||
if raw.id.trim().starts_with("subsonic:") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A NAMESPACED NUMERIC IS A TRACK ROWID BY CONSTRUCTION
|
||||
// (qbz-media-cache mints them from cache rowids), so this arm must not
|
||||
// fire in ALBUM position at all: a Subsonic album id is the server's own
|
||||
// opaque string, never one of these. That alone would have spared the
|
||||
// barcode above, and it is the narrower statement of the two.
|
||||
if raw.kind == Some(ItemKind::Album) {
|
||||
return false;
|
||||
}
|
||||
raw.numeric()
|
||||
.map(|n| RemoteSource::of_id(n as i64) == Some(RemoteSource::Subsonic))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -509,4 +543,34 @@ mod tests {
|
||||
let err = s.playback(&item, &qt(0, 0)).await.unwrap_err();
|
||||
assert!(matches!(err, SourceError::NotConfigured { by: SourceId::SUBSONIC, .. }));
|
||||
}
|
||||
|
||||
/// THE BARCODE THAT BROKE FOUR ALBUMS AT ONCE (2026-08-22).
|
||||
///
|
||||
/// `5099951336752` is George Harrison's "Somewhere In England" on
|
||||
/// EMI/Parlophone — an EAN-13 barcode Qobuz uses as an album id. It has
|
||||
/// bit 42 set and nothing above the payload, i.e. it satisfies the
|
||||
/// Subsonic namespace predicate exactly, so this source used to claim it
|
||||
/// alongside Qobuz and `claim` returned `Ambiguous`.
|
||||
///
|
||||
/// The reserved band is 4_398_046_511_104 ..= 5_497_558_138_879 — EVERY
|
||||
/// 13-digit barcode from 4398… to 5497…, which is all of EMI/Parlophone's
|
||||
/// 5099… prefix. The pre-existing disjointness tests all used
|
||||
/// "0060254702523", whose LEADING ZERO puts it below every floor, so this
|
||||
/// entire class was untested.
|
||||
#[test]
|
||||
fn an_ean13_barcode_in_the_namespace_band_is_not_ours() {
|
||||
let id = "5099951336752";
|
||||
assert_eq!(
|
||||
id.parse::<i64>().unwrap() & (1 << 42),
|
||||
1 << 42,
|
||||
"the barcode really does sit in the reserved band — if this ever \
|
||||
fails the test has stopped covering the case it was written for"
|
||||
);
|
||||
// Named by the caller: the source word must win outright.
|
||||
assert!(!recognises(&RawRef::new("qobuz", ItemKind::Album, id)));
|
||||
// Even unnamed, an ALBUM id is never a namespaced track rowid.
|
||||
let mut unnamed = RawRef::new("qobuz", ItemKind::Album, id);
|
||||
unnamed.source = None;
|
||||
assert!(!recognises(&unnamed));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user