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:
parent
7e2d2db08d
commit
b1f061a2a8
4 changed files with 77 additions and 7 deletions
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue