Add a second playback source and a weighted scheduler mixing it with Navidrome: - Scheduler picks a provider by SOURCE_WEIGHTS, falling through to the others when one has nothing ready, so no source can stall playback. - YtdlpProvider reads a hand-maintained config/urls.txt; container URLs (playlist/album/label/artist) are flat-extracted and one entry is drawn at random, honouring the anti-repeat window. Adds Track.source_url. - YtdlpFetcher downloads bestaudio via the yt-dlp library, reusing the atomic hidden-temp-then-rename pattern; Liquidsoap decodes the result. - Queue now dispatches to a fetcher registry keyed by backend. - Sweep orphaned download temp files on daemon startup (leftovers from a killed container otherwise pile up and trip the stream fallback). Verified end-to-end: yt-dlp opus decoded and served as 192 kbps MP3, and the 3:1 default mix observed in play history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
35 lines
1.2 KiB
Python
35 lines
1.2 KiB
Python
"""Shared data model.
|
|
|
|
A ``Track`` is the uniform object every provider emits: a *resolved* reference
|
|
(which backend can download it, and where) plus display metadata. Fetchers turn
|
|
it into a local file; the queue and the state database use ``key`` for
|
|
de-duplication and anti-repeat.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Track:
|
|
backend: str # which fetcher handles it: "subsonic" | "ytdlp"
|
|
locator: str # backend-specific: Subsonic song id, or a media URL
|
|
artist: str
|
|
title: str
|
|
origin: str # provider that produced it, e.g. "navidrome"
|
|
mbid: str | None = None # filled by the Canonicalizer (milestone 5)
|
|
source_ext: str | None = None # filename hint, e.g. "mp3", "flac"
|
|
source_url: str | None = None # container URL a track was picked from
|
|
|
|
@property
|
|
def key(self) -> str:
|
|
"""Stable identity for de-duplication and anti-repeat.
|
|
|
|
Until the Canonicalizer (milestone 5) fills ``mbid``, we key on the
|
|
backend locator, which is unique within a source.
|
|
"""
|
|
if self.mbid:
|
|
return f"mbid:{self.mbid}"
|
|
return f"{self.backend}:{self.locator}"
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.artist} — {self.title} [{self.origin}]"
|