soulsync/tests/downloads/test_downloads_validation.py
Broque Thomas 6c9b43225a Add torrent and usenet release staging support
Adds torrent/usenet as release-oriented download sources with album-bundle staging, live progress reporting, and post-processing that selects the requested audio file from completed releases instead of blindly importing the first file.

Keeps album-bundle behavior gated to single-source torrent/usenet album downloads, excludes release sources from hybrid album per-track searches, and allows hybrid non-album tracks to use release results safely.

Improves staged-release matching for featured/bonus track filenames while preserving version mismatches, records torrent/usenet provenance in library history, and updates service/status UI labels.

Covers the flow with focused lifecycle, status, staging, validation, task worker, post-processing, and import side-effect tests.
2026-05-21 14:22:21 -07:00

169 lines
6.2 KiB
Python

"""Tests for core/downloads/validation.py — SoundCloud preview filter.
The SoundCloud anonymous tier serves a ~30s preview clip for tracks
gated behind Go+ / login. ``filter_soundcloud_previews`` drops these
candidates before they reach the matcher, the modal cache, or the
manual-pick download path.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from core.downloads import validation
from core.downloads.validation import filter_soundcloud_previews, get_valid_candidates
@dataclass
class _Track:
duration_ms: int
name: str = 'Song'
artists: tuple[str, ...] = ('Artist',)
@dataclass
class _Candidate:
username: str
duration: Optional[int] # milliseconds
title: str = 'Song'
artist: str = 'Artist'
filename: str = 'candidate'
class _MatchingEngine:
def score_track_match(self, **kwargs):
return 0.99, 'core_title_match'
def normalize_string(self, text):
return (text or '').lower()
def test_drops_soundcloud_30s_preview_when_expected_long():
"""A 30s SC candidate against a 5-minute expected track is the
canonical preview-snippet case — must be dropped."""
expected = _Track(duration_ms=338_000) # ~5:38
cands = [
_Candidate(username='soundcloud', duration=30_000, title='Preview'),
_Candidate(username='soundcloud', duration=338_000, title='Real'),
]
result = filter_soundcloud_previews(cands, expected)
assert len(result) == 1
assert result[0].title == 'Real'
def test_drops_under_half_expected_duration():
"""SC candidate at 100s against 300s expected = clearly truncated /
wrong content. Must be dropped even if not at the 30s boundary."""
expected = _Track(duration_ms=300_000)
cand = _Candidate(username='soundcloud', duration=100_000)
assert filter_soundcloud_previews([cand], expected) == []
def test_keeps_soundcloud_when_expected_track_is_short():
"""Genuinely short SC tracks (intros, sound effects, sub-minute
songs) must pass through when the expected track is also short.
Filter only kicks in when expected > 60s."""
expected = _Track(duration_ms=45_000) # 45s expected
cand = _Candidate(username='soundcloud', duration=30_000)
result = filter_soundcloud_previews([cand], expected)
assert result == [cand]
def test_does_not_filter_non_soundcloud_sources():
"""A 30s candidate from another streaming source isn't a SoundCloud
preview — leave it for the generic matching engine to score."""
expected = _Track(duration_ms=338_000)
yt = _Candidate(username='youtube', duration=30_000)
tidal = _Candidate(username='tidal', duration=30_000)
assert filter_soundcloud_previews([yt, tidal], expected) == [yt, tidal]
def test_returns_input_unchanged_without_expected_duration():
"""Without a Spotify-track / expected duration we can't reason
about previews — pass everything through."""
cands = [
_Candidate(username='soundcloud', duration=30_000),
_Candidate(username='soundcloud', duration=300_000),
]
assert filter_soundcloud_previews(cands, None) == cands
assert filter_soundcloud_previews(cands, _Track(duration_ms=0)) == cands
def test_empty_input_returns_empty_list():
assert filter_soundcloud_previews([], _Track(duration_ms=200_000)) == []
def test_keeps_soundcloud_candidate_at_threshold():
"""Boundary check: 35s candidate against 200s expected — exactly
at the 35s preview boundary, but 35s is also above
expected*0.5 (100s) check (35 < 100, so still drops). Use a
higher value to confirm the just-above threshold passes."""
expected = _Track(duration_ms=200_000) # 200s
# 110s passes both checks: > 35s AND > 100s (half of 200s)
cand = _Candidate(username='soundcloud', duration=110_000)
assert filter_soundcloud_previews([cand], expected) == [cand]
def test_rejects_tidal_candidate_that_would_fail_integrity_duration(monkeypatch):
"""Structured sources should not download candidates that post-processing
will immediately quarantine for the same duration mismatch."""
monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
expected = _Track(duration_ms=338_000)
wrong_tidal = _Candidate(username='tidal', duration=30_000)
assert get_valid_candidates([wrong_tidal], expected, 'Artist Song') == []
def test_keeps_tidal_candidate_inside_integrity_duration_tolerance(monkeypatch):
monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
expected = _Track(duration_ms=338_000)
tidal = _Candidate(username='tidal', duration=340_000)
result = get_valid_candidates([tidal], expected, 'Artist Song')
assert result == [tidal]
def test_rejects_torrent_title_match_from_wrong_artist(monkeypatch):
monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
wrong_artist = _Candidate(
username='torrent',
duration=None,
title='The Man I Need',
artist='Tinkabelle',
)
assert get_valid_candidates([wrong_artist], expected, 'Olivia Dean The Man I Need') == []
def test_keeps_torrent_title_match_from_expected_artist(monkeypatch):
monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
correct_artist = _Candidate(
username='torrent',
duration=None,
title='The Man I Need',
artist='Olivia Dean',
)
result = get_valid_candidates([correct_artist], expected, 'Olivia Dean The Man I Need')
assert result == [correct_artist]
def test_keeps_torrent_title_match_when_artist_is_indexer_fallback(monkeypatch):
monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
candidate = _Candidate(
username='torrent',
duration=None,
title='The Man I Need',
artist='Indexer',
)
candidate._source_metadata = {'indexer': 'Indexer'}
result = get_valid_candidates([candidate], expected, 'Olivia Dean The Man I Need')
assert result == [candidate]