manual search: float the pasted Qobuz/Tidal track to the top (#932)

a pasted track link IS resolved + searched, but the 'bubble the exact track to the top'
step read getattr(t,'id') — and TrackResult has no top-level id (the source id lives in
_source_metadata['track_id']). so the bubble was a silent no-op: the linked track sat buried
among fuzzy text-search lookalikes and the user saw unrelated tracks. qobuz made it worse —
_qobuz_to_track_result never stamped _source_metadata at all, so the track had no id to match.

- stamp _source_metadata={'source':'qobuz','track_id':...} on qobuz TrackResults (mirrors tidal)
- extract the bubble into pure, tested helpers (linked_track_id / bubble_linked_track_first)
  that read _source_metadata['track_id'] — fixes it for tidal too, str/int-safe, stable no-op
19 track-link tests (+6 new) + 87 qobuz/download tests green.
This commit is contained in:
BoulderBadgeDad 2026-06-26 21:45:41 -07:00
parent 7e2d2db08d
commit b1f061a2a8
4 changed files with 77 additions and 7 deletions

View file

@ -13,9 +13,31 @@ Pure + import-safe: parsing only, no network.
from __future__ import annotations
import re
from typing import Any, Optional, Tuple
from typing import Any, List, Optional, Tuple
from urllib.parse import urlparse
def linked_track_id(track: Any) -> str:
"""The source track id stamped on a search result, read from
``_source_metadata['track_id']`` the field every ID-downloadable source
(Tidal, Qobuz) records. Empty string when absent. ``TrackResult`` has no
top-level ``id``, so callers must NOT use ``getattr(t, 'id')`` (that always
missed and left the pasted-link bubble a silent no-op #932)."""
meta = getattr(track, '_source_metadata', None)
if not isinstance(meta, dict):
return ''
return str(meta.get('track_id') or '')
def bubble_linked_track_first(tracks: List[Any], link_track_id: str) -> List[Any]:
"""Float the result whose source id matches a pasted link to the top so the
user sees the EXACT track they linked, not a fuzzy text-search lookalike
(#813/#932). Stable + a graceful no-op when no result carries the id."""
if not link_track_id or not tracks:
return tracks
target = str(link_track_id)
return sorted(tracks, key=lambda t: linked_track_id(t) != target)
# host substring → download source id. Only ID-downloadable streaming sources.
_HOSTS = (
('tidal.com', 'tidal'),

View file

@ -1074,6 +1074,10 @@ class QobuzClient(DownloadSourcePlugin):
title=title,
album=album_name,
track_number=track.get('track_number'),
# Stamp the Qobuz track id so a pasted-link manual search can float the
# exact track to the top (#932). Without this the bubble never matched
# and the linked track stayed buried among fuzzy lookalikes.
_source_metadata={'source': 'qobuz', 'track_id': str(track.get('id') or '')},
)
# Stamp real API quality so the global ranker sees actual

View file

@ -78,3 +78,47 @@ def test_payload_non_dict_or_empty():
assert q('tidal', None) is None
assert q('tidal', {}) is None
assert q('qobuz', 'garbage') is None
# ── bubble the pasted-link track to the top (#932) ──
from types import SimpleNamespace
from core.downloads.track_link import linked_track_id, bubble_linked_track_first
def _result(track_id=None):
"""Mimic a TrackResult: NO top-level `id`, the source id lives in
_source_metadata['track_id'] (or absent entirely)."""
meta = {'source': 'qobuz', 'track_id': track_id} if track_id is not None else None
return SimpleNamespace(_source_metadata=meta, title='t')
def test_linked_track_id_reads_source_metadata():
assert linked_track_id(_result('296427754')) == '296427754'
def test_linked_track_id_empty_when_absent():
# the exact #932 trap: there is no top-level `id`, so getattr(t,'id') would miss.
r = _result()
assert not hasattr(r, 'id')
assert linked_track_id(r) == ''
def test_bubble_floats_exact_track_to_top():
fuzzy_a, exact, fuzzy_b = _result('111'), _result('296427754'), _result('222')
out = bubble_linked_track_first([fuzzy_a, exact, fuzzy_b], '296427754')
assert out[0] is exact # exact track surfaced first
assert out[1:] == [fuzzy_a, fuzzy_b] # stable order for the rest
def test_bubble_handles_int_vs_str_id():
out = bubble_linked_track_first([_result('999'), _result('296427754')], 296427754)
assert linked_track_id(out[0]) == '296427754'
def test_bubble_noop_when_nothing_matches_or_empty():
a, b = _result('1'), _result('2')
assert bubble_linked_track_first([a, b], '296427754') == [a, b] # unchanged
assert bubble_linked_track_first([], '296427754') == []
assert bubble_linked_track_first([a, b], '') == [a, b]

View file

@ -7533,13 +7533,13 @@ def manual_search_for_task(task_id):
"error": error,
}) + "\n"
continue
# Pasted-link exact match: bubble the track whose id matches
# the link to the top so the user sees the exact version
# first (graceful no-op if ids don't line up).
# Pasted-link exact match: bubble the track whose source id
# matches the link to the top so the user sees the exact version
# first. Reads _source_metadata['track_id'] (TrackResult has no
# top-level id) — the old getattr(t,'id') always missed (#932).
if src_name == link_source and link_track_id and tracks:
tracks = sorted(
tracks,
key=lambda t: str(getattr(t, 'id', '')) != str(link_track_id))
from core.downloads.track_link import bubble_linked_track_first
tracks = bubble_linked_track_first(tracks, link_track_id)
serialized = []
for t in tracks:
s = _serialize_candidate(t, source_override=src_name)