radieo/ingest/radieo/models.py
Pierre-Olivier Mercier 8c27498632 Milestone 3: Navidrome (OpenSubsonic) playback provider
Replace the directory-scan queue with a real ingestion pipeline:
provider -> fetcher -> cache -> ready queue, driven by a background
prefetch thread.

- subsonic.py: minimal OpenSubsonic client (salted-token auth,
  getPlaylists/getPlaylist, raw streaming download).
- providers/navidrome.py: pick tracks from a playlist (by name or id),
  with anti-repeat and periodic playlist reload.
- fetchers/subsonic.py: atomic download into the shared cache.
- db.py: SQLite state — append-only play history (anti-repeat + stats)
  and cache_files LRU retention (keep the N most recently played).
- queue.py: prefetch buffer + retention on play; graceful degradation
  to the stream's local-cache fallback when no source is configured.
- api.py: GET /next now carries real title/artist metadata.
- Config via .env (Navidrome credentials), persistent state/ volume,
  httpx dependency.

Verified end-to-end against a live Navidrome: playlist resolved,
tracks downloaded and broadcast, retention and history correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:57:38 +08:00

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