From 1c013dfdd175e7a815f59f1529458082b0cfcc0e Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 10 Jul 2026 18:22:47 +0200 Subject: [PATCH] feat: add domain types and quality ranking --- worker/lyra_worker/quality.py | 23 ++++++++++++++++++++++ worker/lyra_worker/types.py | 37 +++++++++++++++++++++++++++++++++++ worker/tests/test_quality.py | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 worker/lyra_worker/quality.py create mode 100644 worker/lyra_worker/types.py create mode 100644 worker/tests/test_quality.py diff --git a/worker/lyra_worker/quality.py b/worker/lyra_worker/quality.py new file mode 100644 index 0000000..e40db1d --- /dev/null +++ b/worker/lyra_worker/quality.py @@ -0,0 +1,23 @@ +from lyra_worker.types import Candidate, Quality + +_SOURCE_TIERS = {"qobuz": 0, "soulseek": 1, "youtube": 2} + + +def quality_class(q: Quality) -> int: + """3 = hi-res lossless, 2 = CD lossless, 1 = lossy.""" + if q.lossless: + hires = (q.bit_depth is not None and q.bit_depth > 16) or ( + q.sample_rate is not None and q.sample_rate > 44100 + ) + return 3 if hires else 2 + return 1 + + +def source_tier(source: str) -> int: + """Lower is better. Unknown sources rank worst.""" + return _SOURCE_TIERS.get(source, 99) + + +def rank_key(c: Candidate) -> tuple[int, int]: + """Sort key; larger is better. Quality class first, then source tier.""" + return (quality_class(c.quality), -source_tier(c.source)) diff --git a/worker/lyra_worker/types.py b/worker/lyra_worker/types.py new file mode 100644 index 0000000..cac640e --- /dev/null +++ b/worker/lyra_worker/types.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class MBTarget: + artist: str + album: str + track_count: int | None = None + total_duration_s: int | None = None + + +@dataclass(frozen=True) +class Quality: + fmt: str + lossless: bool + bit_depth: int | None = None + sample_rate: int | None = None + bitrate_kbps: int | None = None + + +@dataclass(frozen=True) +class Candidate: + source: str + source_ref: str + matched_artist: str + matched_album: str + quality: Quality + track_count: int + confidence: float = 0.0 + + +@dataclass(frozen=True) +class DownloadResult: + ok: bool + path: str | None = None + track_count: int = 0 + error: str | None = None diff --git a/worker/tests/test_quality.py b/worker/tests/test_quality.py new file mode 100644 index 0000000..8a45be2 --- /dev/null +++ b/worker/tests/test_quality.py @@ -0,0 +1,37 @@ +from lyra_worker.quality import quality_class, source_tier, rank_key +from lyra_worker.types import Candidate, Quality + +HIRES = Quality(fmt="FLAC", lossless=True, bit_depth=24, sample_rate=96000) +CD = Quality(fmt="FLAC", lossless=True, bit_depth=16, sample_rate=44100) +MP3 = Quality(fmt="MP3", lossless=False, bitrate_kbps=320) + + +def _cand(source, quality): + return Candidate(source=source, source_ref="x", matched_artist="A", + matched_album="B", quality=quality, track_count=10) + + +def test_quality_class_tiers(): + assert quality_class(HIRES) == 3 + assert quality_class(CD) == 2 + assert quality_class(MP3) == 1 + + +def test_source_tier_order(): + assert source_tier("qobuz") == 0 + assert source_tier("soulseek") == 1 + assert source_tier("youtube") == 2 + + +def test_design_ranking_order(): + # Qobuz hi-res > Qobuz lossless > Soulseek FLAC > Soulseek MP3 > YouTube + ordered = [ + _cand("qobuz", HIRES), + _cand("qobuz", CD), + _cand("soulseek", CD), + _cand("soulseek", MP3), + _cand("youtube", MP3), + ] + ranks = [rank_key(c) for c in ordered] + assert ranks == sorted(ranks, reverse=True) + assert len(set(ranks)) == 5 # strict total order, no ties