Drop SoundCloud preview snippets before scoring
SoundCloud serves a ~30s preview clip for tracks gated behind Go+ or login (extremely common for major-label uploads — what's actually on SoundCloud is bootlegs, fan reuploads, type beats, and these previews). yt-dlp accepts the preview as the download payload, the post-download integrity check catches the duration mismatch and quarantines the file, but the user only sees "all candidates failed" with no obvious explanation. Filter at validation time when we know expected_duration: drop SoundCloud candidates whose duration is below half the expected length OR within ~5s of the 30s preview boundary, gated on expected being non-trivially long (>60s) so genuinely short tracks still pass through.
This commit is contained in:
parent
d17365296a
commit
563204ceae
2 changed files with 24 additions and 0 deletions
|
|
@ -50,6 +50,29 @@ def get_valid_candidates(results, spotify_track, query):
|
|||
|
||||
scored = []
|
||||
for r in results:
|
||||
# SoundCloud-specific: drop preview snippets BEFORE scoring.
|
||||
# Anonymous SoundCloud serves a ~30s preview clip for tracks
|
||||
# gated behind Go+ / login. yt-dlp accepts these as the
|
||||
# download payload, the integrity check catches them
|
||||
# post-download, but the user just sees "all candidates
|
||||
# failed". Filter at search time when we know expected
|
||||
# duration so the matcher never picks a 30s clip for a
|
||||
# 5-minute track. Keep candidates that genuinely are short
|
||||
# (intros, sound effects) when the expected track is also
|
||||
# short.
|
||||
if r.username == 'soundcloud' and expected_duration and r.duration:
|
||||
cand_secs = r.duration / 1000.0
|
||||
expected_secs = expected_duration / 1000.0
|
||||
# Drop if expected is non-trivially long AND candidate is
|
||||
# near the SoundCloud 30s preview boundary OR less than
|
||||
# half the expected duration.
|
||||
if expected_secs > 60 and (cand_secs < 35 or cand_secs < expected_secs * 0.5):
|
||||
logger.info(
|
||||
f"[SoundCloud] Dropping preview/short candidate "
|
||||
f"'{r.title}' ({cand_secs:.1f}s vs expected {expected_secs:.1f}s)"
|
||||
)
|
||||
continue
|
||||
|
||||
# Score using matching engine's generic scorer (same weights as Soulseek)
|
||||
confidence, match_type = matching_engine.score_track_match(
|
||||
source_title=expected_title,
|
||||
|
|
|
|||
|
|
@ -3432,6 +3432,7 @@ const WHATS_NEW = {
|
|||
'2.4.2': [
|
||||
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
|
||||
{ date: 'Unreleased — 2.4.2 dev cycle' },
|
||||
{ title: 'Drop SoundCloud Preview Snippets at Search Time', desc: 'soundcloud serves a ~30s preview clip for tracks that are gated behind go+ / login (very common for major-label uploads — official content basically doesn\'t exist on soundcloud, so what shows up is bootlegs, fan uploads, type beats, and these 30s previews). yt-dlp accepts the preview as the download payload, the post-download integrity check catches the duration mismatch and quarantines the file, but the user just sees "all candidates failed" with no explanation. now the search-time filter drops candidates whose duration is below half the expected length OR within ~5s of the 30s preview boundary, but only when the expected track is non-trivially long (>60s) so genuinely short tracks (intros, sound effects, sub-minute songs) still pass through.', page: 'downloads' },
|
||||
{ title: 'Internal: Move Shared Download Dataclasses + Singleton Boot Path', desc: 'internal — two architectural cleanups on top of the download engine refactor. (1) `TrackResult`, `AlbumResult`, `DownloadStatus`, `SearchResult` lived in `core/soulseek_client.py` for historical reasons (they grew up there as the soulseek-only types and got exported when other download sources were added). every plugin imported these from the soulseek module just to satisfy the contract — coupling 8 clients to a sibling source for type imports only. moved them to `core/download_plugins/types.py` (the neutral plugin package) and updated all 14 import sites across deezer/hifi/lidarr/qobuz/soundcloud/tidal/youtube clients + the engine + matching engine + redownload + tests. clean break, no backward-compat re-export. (2) `web_server.py` now boots the orchestrator via `set_download_orchestrator(DownloadOrchestrator())` so the singleton factory + boot path share state — `get_download_orchestrator()` returns the same instance the global handle points at instead of lazily building a separate one. matches cin\'s `get_metadata_engine()` pattern.' },
|
||||
{ title: 'Internal: Rename `soulseek_client` Global → `download_orchestrator`', desc: 'internal — followup cleanup. the global handle in web_server.py was named `soulseek_client` for historical reasons (the orchestrator was originally just the soulseek client and grew downstream sources around it), but the type has long been `DownloadOrchestrator` not `SoulseekClient`. renamed the global + every parameter/attribute that carried the legacy name across web_server.py, api/, core/downloads/*, core/search/*, core/streaming/*, services/sync_service.py, and the test fixtures (`MasterDeps.soulseek_client` → `download_orchestrator`, `init(soulseek_client_obj)` → `init(download_orchestrator_obj)`, etc). module path `core.soulseek_client` and class `SoulseekClient` (the actual soulseek-only client) are unchanged — only the orchestrator handle renamed. ~250 references touched, suite green.' },
|
||||
{ title: 'Internal: Drop Backward-Compat Per-Source Attrs', desc: 'internal — followup to cin\'s download engine review. removed the `orchestrator.soulseek` / `.youtube` / `.tidal` / `.qobuz` / `.hifi` / `.deezer_dl` / `.lidarr` / `.soundcloud` attribute aliases that were preserved for backward compat. external callers (core/downloads/, core/search/, web_server.py) all migrated to the generic `orchestrator.client(\'<source>\')` accessor — alias-aware (legacy `deezer_dl` resolves to canonical `deezer`), single source of truth via the registry. the orchestrator\'s own internal `self.soulseek` / `self.deezer_dl` reaches also routed through `client()` so the only place that knows about per-source identity is the registry. test fakes updated to expose `client(name)` instead of stuffing attributes; conformance test pinned to the new accessor contract. zero behavior change — just cleaner shape.' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue