soulsync/tests/downloads/test_downloads_candidates.py
BoulderBadgeDad 2604704a27 #797: stop AcoustID quarantining correct non-English-artist downloads
AcoustID returns a recording's title/artist in their ORIGINAL script
(e.g. "久石譲" for Joe Hisaishi) while SoulSync's expected metadata is
romanized/English. A correct download then fails verification on two
walls: the title can never clear the 0.70 similarity bar cross-script,
and the only skip path that ignores the title required a near-perfect
0.95 fingerprint plus a resolved alias. Result: every non-English
artist trips it. Two complementary fixes, per the reporter's two ideas.

Graceful fix (automatic):
- New pure core/matching/script_compat.py detects when two strings are
  in genuinely different writing systems (CJK/Hangul/Cyrillic/Greek/
  Arabic/Hebrew/Thai vs Latin). Accented Latin (Beyoncé, Sigur Rós)
  stays Latin — no false trigger.
- acoustid_verification.py: when the EXPECTED artist and the matched
  artist span scripts AND the artist is confirmed via the existing
  MusicBrainz alias bridge, SKIP instead of quarantine, without the
  0.95 floor (the 0.80 trust floor already gates the fingerprint).
- Deliberately narrow: keyed on the ARTIST spanning scripts + being
  confirmed. A same-script artist with only a cross-script title keeps
  the stricter 0.95 floor, so the #607 wrong-file protection (Kendrick
  R.O.T.C, low-fingerprint Japanese-title) is untouched.

Per-request toggle (manual escape hatch):
- New "Skip AcoustID verification" checkbox in the download-missing
  modal beside "Force Download All".
- skip_acoustid threads request -> batch -> per-track track_info ->
  download context (same path as _playlist_folder_mode), landing on
  the existing _skip_quarantine_check='acoustid' bypass. No new
  mechanism; only the AcoustID gate is bypassed (integrity/bit-depth
  still run).

Tests:
- tests/matching/test_script_compat.py — script-boundary cases.
- test_acoustid_skip_logic.py — Joe Hisaishi SKIPs at 0.85; unconfirmed
  cross-script artist still FAILs; same-script low-fingerprint still
  FAILs.
- test_downloads_candidates.py — toggle injects the bypass; absent
  toggle keeps verification.

Full suite: 5169 passed; only pre-existing soundcloud /app env failures
remain. Zero regressions.
2026-06-05 16:02:01 -07:00

517 lines
19 KiB
Python

"""Tests for core/downloads/candidates.py — candidate fallback download logic."""
from __future__ import annotations
import threading
from dataclasses import dataclass
import pytest
from core.downloads import candidates as dc
from core.runtime_state import (
download_tasks,
matched_context_lock,
matched_downloads_context,
tasks_lock,
)
# ---------------------------------------------------------------------------
# Fixtures + fakes
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def reset_state():
download_tasks.clear()
matched_downloads_context.clear()
yield
download_tasks.clear()
matched_downloads_context.clear()
@dataclass
class _Candidate:
username: str = "user1"
filename: str = "song.flac"
confidence: float = 0.9
size: int = 1000
title: str = "Song"
artist: str = "Artist"
album: str = "Album"
quality_score: float = 0.0
upload_speed: int = 0
queue_length: int = 0
free_upload_slots: int = 0
@dataclass
class _Track:
name: str = "Song Title"
album: str = "Album Name"
artists: list = None
id: str = "spt-1"
def __post_init__(self):
if self.artists is None:
self.artists = ["Artist Name"]
class _FakeSoulseek:
def __init__(self, download_id="dl-1"):
self._download_id = download_id
self.download_calls = []
self.cancel_calls = []
async def download(self, username, filename, size):
self.download_calls.append((username, filename, size))
return self._download_id
async def cancel_download(self, download_id, username, remove=True):
self.cancel_calls.append((download_id, username, remove))
class _FakeSpotify:
def __init__(self, track_details=None):
self._track_details = track_details
def get_track_details(self, track_id):
return self._track_details
class _FakeDB:
def __init__(self, blacklisted=None):
self._blacklisted = blacklisted or set()
def is_blacklisted(self, username, filename):
return (username, filename) in self._blacklisted
def _run_async(coro):
"""Drive async functions synchronously for tests."""
import asyncio
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
def _build_deps(
*,
soulseek=None,
spotify=None,
db=None,
update_status=None,
on_complete=None,
):
deps = dc.CandidatesDeps(
download_orchestrator=soulseek or _FakeSoulseek(),
spotify_client=spotify or _FakeSpotify(),
run_async=_run_async,
get_database=lambda: db or _FakeDB(),
update_task_status=update_status or (lambda task_id, status: None),
make_context_key=lambda u, f: f"{u}::{f}",
on_download_completed=on_complete or (lambda *a, **kw: None),
)
return deps
def _seed_task(task_id, *, status="pending", track_info=None, used_sources=None,
download_id=None):
download_tasks[task_id] = {
"status": status,
"track_info": track_info or {},
"used_sources": used_sources or set(),
"download_id": download_id,
}
# ---------------------------------------------------------------------------
# Happy path — first candidate succeeds
# ---------------------------------------------------------------------------
def test_first_candidate_starts_download_and_returns_true():
"""High-confidence candidate accepts → download_id stored, context populated, returns True."""
deps = _build_deps()
_seed_task("t1")
candidates = [_Candidate(filename="best.flac", confidence=0.95)]
track = _Track()
result = dc.attempt_download_with_candidates("t1", candidates, track, batch_id="b1", deps=deps)
assert result is True
assert deps.download_orchestrator.download_calls == [("user1", "best.flac", 1000)]
assert download_tasks["t1"]["download_id"] == "dl-1"
assert "user1::best.flac" in matched_downloads_context
def test_candidates_tried_in_confidence_order():
"""Multiple candidates → tried highest-confidence first."""
deps = _build_deps()
_seed_task("t2")
candidates = [
_Candidate(filename="low.flac", confidence=0.5),
_Candidate(filename="high.flac", confidence=0.95),
_Candidate(filename="mid.flac", confidence=0.7),
]
track = _Track()
dc.attempt_download_with_candidates("t2", candidates, track, batch_id=None, deps=deps)
# First call should be the highest-confidence one
assert deps.download_orchestrator.download_calls[0][1] == "high.flac"
# ---------------------------------------------------------------------------
# used_sources dedupe
# ---------------------------------------------------------------------------
def test_already_tried_source_skipped():
"""Source in used_sources is skipped (no duplicate download attempt)."""
deps = _build_deps()
_seed_task("t3", used_sources={"user1_already.flac"})
candidates = [
_Candidate(filename="already.flac", confidence=0.9),
_Candidate(filename="fresh.flac", confidence=0.85),
]
track = _Track()
dc.attempt_download_with_candidates("t3", candidates, track, batch_id=None, deps=deps)
# First candidate skipped (already used), second one tried
assert len(deps.download_orchestrator.download_calls) == 1
assert deps.download_orchestrator.download_calls[0][1] == "fresh.flac"
# ---------------------------------------------------------------------------
# Blacklist
# ---------------------------------------------------------------------------
def test_blacklisted_source_skipped():
"""Blacklisted candidate is skipped."""
db = _FakeDB(blacklisted={("user1", "blacklisted.flac")})
deps = _build_deps(db=db)
_seed_task("t4")
candidates = [
_Candidate(filename="blacklisted.flac", confidence=0.95),
_Candidate(filename="ok.flac", confidence=0.85),
]
track = _Track()
dc.attempt_download_with_candidates("t4", candidates, track, batch_id=None, deps=deps)
assert deps.download_orchestrator.download_calls[0][1] == "ok.flac"
# ---------------------------------------------------------------------------
# Cancellation paths
# ---------------------------------------------------------------------------
def test_cancellation_before_attempt_returns_false():
"""status=cancelled at top of loop → return False, no download attempted."""
deps = _build_deps()
_seed_task("t5", status="cancelled")
candidates = [_Candidate()]
track = _Track()
result = dc.attempt_download_with_candidates("t5", candidates, track, batch_id=None, deps=deps)
assert result is False
assert deps.download_orchestrator.download_calls == []
def test_task_deleted_returns_false():
"""Task removed from download_tasks mid-loop → return False."""
deps = _build_deps()
# Task NOT seeded — looks deleted
candidates = [_Candidate()]
track = _Track()
result = dc.attempt_download_with_candidates("missing", candidates, track, batch_id=None, deps=deps)
assert result is False
def test_active_download_id_skips_new_download():
"""If task already has download_id, candidate skipped (race protection)."""
deps = _build_deps()
_seed_task("t6", download_id="existing-dl")
candidates = [_Candidate(), _Candidate(filename="other.flac")]
track = _Track()
dc.attempt_download_with_candidates("t6", candidates, track, batch_id=None, deps=deps)
# Both candidates skipped (download_id already present)
assert deps.download_orchestrator.download_calls == []
def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
"""If task is cancelled after download_id assigned, cancel_download fires + on_complete(False)."""
completion_calls = []
deps = _build_deps(on_complete=lambda batch_id, task_id, success=None: completion_calls.append((batch_id, task_id, success)))
_seed_task("t7")
candidates = [_Candidate()]
track = _Track()
# Simulate cancel happening between download_id assignment and final lock check.
# update_task_status is the callback that runs RIGHT after the download starts.
# We use it to flip status to cancelled.
def cancel_mid_flight(task_id, status):
if status == "downloading":
with tasks_lock:
if task_id in download_tasks:
pass # status set legitimately
# No-op here; we'll cancel via the lock directly below
download_tasks[task_id]["status"] = "cancelled"
deps.update_task_status = cancel_mid_flight
result = dc.attempt_download_with_candidates("t7", candidates, track, batch_id="b7", deps=deps)
assert result is False
# cancel_download was called for the in-flight transfer
assert deps.download_orchestrator.cancel_calls
# on_download_completed fired with success=False to free the worker slot
assert completion_calls == [("b7", "t7", False)]
# ---------------------------------------------------------------------------
# Failure path — all candidates exhausted
# ---------------------------------------------------------------------------
def test_all_candidates_failed_returns_false():
"""If download_orchestrator.download returns None (failure) for all candidates, returns False."""
soulseek = _FakeSoulseek(download_id=None)
deps = _build_deps(soulseek=soulseek)
_seed_task("t8")
candidates = [_Candidate(filename="c1.flac"), _Candidate(filename="c2.flac")]
track = _Track()
result = dc.attempt_download_with_candidates("t8", candidates, track, batch_id=None, deps=deps)
assert result is False
# Both candidates were tried
assert len(soulseek.download_calls) == 2
def test_exception_during_download_continues_to_next_candidate():
"""An exception on one candidate → continue to the next."""
call_count = [0]
class _FlakySoulseek(_FakeSoulseek):
async def download(self, username, filename, size):
call_count[0] += 1
if call_count[0] == 1:
raise RuntimeError("network blip")
return "dl-2"
soulseek = _FlakySoulseek()
deps = _build_deps(soulseek=soulseek)
_seed_task("t9")
candidates = [_Candidate(filename="c1.flac"), _Candidate(filename="c2.flac")]
track = _Track()
result = dc.attempt_download_with_candidates("t9", candidates, track, batch_id=None, deps=deps)
assert result is True
assert download_tasks["t9"]["download_id"] == "dl-2"
# ---------------------------------------------------------------------------
# Context payload
# ---------------------------------------------------------------------------
def test_explicit_album_context_uses_real_album_data():
"""track_info with _is_explicit_album_download=True copies real album/artist context."""
deps = _build_deps()
explicit_album = {
"id": "alb-real",
"name": "Real Album",
"release_date": "2024-05-05",
"total_tracks": 12,
"total_discs": 2,
"album_type": "album",
"image_url": "http://img/a.jpg",
}
explicit_artist = {"id": "art-real", "name": "Real Artist"}
_seed_task("t10", track_info={
"_is_explicit_album_download": True,
"_explicit_album_context": explicit_album,
"_explicit_artist_context": explicit_artist,
"track_number": 5,
"disc_number": 2,
})
candidates = [_Candidate(filename="explicit.flac")]
track = _Track(album="Real Album", artists=["Real Artist"])
dc.attempt_download_with_candidates("t10", candidates, track, batch_id=None, deps=deps)
ctx = matched_downloads_context["user1::explicit.flac"]
assert ctx["spotify_album"]["id"] == "alb-real"
assert ctx["spotify_album"]["total_discs"] == 2
assert ctx["spotify_artist"]["id"] == "art-real"
assert ctx["is_album_download"] is True
def test_track_number_from_track_info_preferred_over_api():
"""track_number from track_info wins over track object and API."""
api_track = {"track_number": 99, "disc_number": 9, "album": {}}
deps = _build_deps(spotify=_FakeSpotify(track_details=api_track))
_seed_task("t11", track_info={"track_number": 5, "disc_number": 1})
candidates = [_Candidate()]
track = _Track()
dc.attempt_download_with_candidates("t11", candidates, track, batch_id=None, deps=deps)
enhanced = matched_downloads_context["user1::song.flac"]["original_search_result"]
assert enhanced["track_number"] == 5
assert enhanced["disc_number"] == 1
def test_api_backfills_album_context_when_missing():
"""When local album context is incomplete, Spotify API backfills release_date / album_type."""
api_track = {
"track_number": 7,
"disc_number": 1,
"album": {
"id": "alb-from-api",
"release_date": "2025-01-01",
"album_type": "album",
"total_tracks": 10,
"images": [{"url": "http://api/img.jpg"}],
},
}
deps = _build_deps(spotify=_FakeSpotify(track_details=api_track))
# No track_info track_number → triggers API fallback
_seed_task("t12")
candidates = [_Candidate()]
track = _Track(album="Album Name") # truthy but no detailed metadata locally
dc.attempt_download_with_candidates("t12", candidates, track, batch_id=None, deps=deps)
ctx = matched_downloads_context["user1::song.flac"]
# release_date defaults to '' in the fallback context, so backfill fires.
assert ctx["spotify_album"]["release_date"] == "2025-01-01"
# Note: id stays "from_sync_modal" because the fallback assigns a non-empty
# placeholder, and the backfill only fires when `not spotify_album_context.get('id')`.
# The current behavior is what production does — assertion documents that.
assert ctx["spotify_album"]["id"] == "from_sync_modal"
# ---------------------------------------------------------------------------
# Sort by confidence is stable for equal scores
# ---------------------------------------------------------------------------
def test_candidates_with_equal_confidence_both_tried():
"""Equal-confidence candidates are tried in their existing order."""
deps = _build_deps()
_seed_task("t13")
candidates = [
_Candidate(filename="a.flac", confidence=0.9),
_Candidate(filename="b.flac", confidence=0.9),
]
track = _Track()
dc.attempt_download_with_candidates("t13", candidates, track, batch_id=None, deps=deps)
# First one wins — second never tried because download succeeded
assert len(deps.download_orchestrator.download_calls) == 1
assert deps.download_orchestrator.download_calls[0][1] == "a.flac"
def test_user_manual_pick_injects_acoustid_bypass_into_post_process_context():
"""Issue #701: when the user picks a specific candidate via the
candidates modal, the download_selected_candidate endpoint sets
`_user_manual_pick=True` on the task. The candidates helper must
propagate that into the stored post-process context as
`_skip_quarantine_check='acoustid'`; without it the manual pick
loops straight back into quarantine whenever AcoustID disagrees
with the user's selection."""
deps = _build_deps()
_seed_task("t_manual_pick")
download_tasks["t_manual_pick"]["_user_manual_pick"] = True
candidates = [_Candidate(filename="picked.flac", confidence=0.99)]
track = _Track()
result = dc.attempt_download_with_candidates("t_manual_pick", candidates, track, batch_id="b1", deps=deps)
assert result is True
ctx = matched_downloads_context["user1::picked.flac"]
assert ctx["_skip_quarantine_check"] == "acoustid"
assert ctx["_user_manual_pick"] is True
def test_auto_search_pick_does_not_inject_acoustid_bypass():
"""The bypass is ONLY for user-initiated manual picks. Auto-search
candidate picks (which run during the normal download flow) must
still get AcoustID verification — they're the canonical guard
against the wrong-file leak that quarantine exists to catch."""
deps = _build_deps()
_seed_task("t_auto_pick") # No _user_manual_pick flag set
candidates = [_Candidate(filename="auto.flac", confidence=0.99)]
track = _Track()
result = dc.attempt_download_with_candidates("t_auto_pick", candidates, track, batch_id="b1", deps=deps)
assert result is True
ctx = matched_downloads_context["user1::auto.flac"]
assert "_skip_quarantine_check" not in ctx
assert "_user_manual_pick" not in ctx
def test_skip_acoustid_track_flag_injects_bypass():
"""Issue #797: when the album-download request had the per-request
'Skip AcoustID verification' toggle on, the master worker stamps
`_skip_acoustid=True` onto each track's track_info. The candidates
helper must propagate that into the post-process context as
`_skip_quarantine_check='acoustid'` so AcoustID never quarantines
this request's files (e.g. correct downloads of non-English artists
whose native-script metadata AcoustID can't reconcile)."""
deps = _build_deps()
_seed_task("t_skip_aid", track_info={"_skip_acoustid": True})
candidates = [_Candidate(filename="skip.flac", confidence=0.99)]
track = _Track()
result = dc.attempt_download_with_candidates("t_skip_aid", candidates, track, batch_id="b1", deps=deps)
assert result is True
ctx = matched_downloads_context["user1::skip.flac"]
assert ctx["_skip_quarantine_check"] == "acoustid"
# It's the toggle path, NOT a manual pick.
assert "_user_manual_pick" not in ctx
def test_no_skip_acoustid_flag_keeps_verification():
"""Without the toggle (no `_skip_acoustid` on track_info), AcoustID
verification must still run — the bypass is opt-in per request."""
deps = _build_deps()
_seed_task("t_no_skip_aid", track_info={}) # no _skip_acoustid
candidates = [_Candidate(filename="verify.flac", confidence=0.99)]
track = _Track()
result = dc.attempt_download_with_candidates("t_no_skip_aid", candidates, track, batch_id="b1", deps=deps)
assert result is True
ctx = matched_downloads_context["user1::verify.flac"]
assert "_skip_quarantine_check" not in ctx
def test_equal_confidence_candidates_prefer_better_peer_quality():
"""Equal-confidence Soulseek candidates use peer quality as the tiebreaker."""
deps = _build_deps()
_seed_task("t14")
candidates = [
_Candidate(filename="slow.flac", confidence=0.9, quality_score=0.8,
upload_speed=100_000, queue_length=0, free_upload_slots=1),
_Candidate(filename="fast.flac", confidence=0.9, quality_score=1.0,
upload_speed=5_000_000, queue_length=0, free_upload_slots=1),
]
track = _Track()
dc.attempt_download_with_candidates("t14", candidates, track, batch_id=None, deps=deps)
assert deps.download_orchestrator.download_calls[0][1] == "fast.flac"