radieo/ingest/radieo/models.py
Pierre-Olivier Mercier d1db6a11d8 Milestone 4: yt-dlp provider and weighted source scheduler
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>
2026-07-02 17:58:24 +08:00

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}]"