feat: add domain types and quality ranking
This commit is contained in:
@@ -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))
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user