ingest: rename the Navidrome source to OpenSubsonic
The provider only uses standard OpenSubsonic endpoints (getPlaylists, getPlaylist, search3, stream), so it works with any compatible server (Navidrome, Gonic, Airsonic…), not just Navidrome. BREAKING: environment variables are renamed RADIEO_NAVIDROME_URL/USER/PASSWORD/PLAYLIST -> RADIEO_SUBSONIC_* RADIEO_WEIGHT_NAVIDROME -> RADIEO_WEIGHT_SUBSONIC Update your .env accordingly. Internally the source key and provider are renamed navidrome -> subsonic, aligning with the existing 'subsonic' backend and fetcher. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
85cd5d1b74
commit
9cc2ede37d
8 changed files with 49 additions and 47 deletions
14
.env.example
14
.env.example
|
|
@ -1,14 +1,14 @@
|
||||||
# radieo — configuration locale. Copier en `.env` et remplir.
|
# radieo — configuration locale. Copier en `.env` et remplir.
|
||||||
# docker compose lit automatiquement `.env` pour ces variables.
|
# docker compose lit automatiquement `.env` pour ces variables.
|
||||||
|
|
||||||
# --- Source Navidrome / OpenSubsonic ---
|
# --- Source OpenSubsonic (Navidrome, Gonic, Airsonic…) ---
|
||||||
# URL de base de ton serveur (sans /rest). Laisser les champs vides désactive
|
# URL de base de ton serveur (sans /rest). Laisser les champs vides désactive
|
||||||
# la source : le stream joue alors uniquement les fichiers déjà dans cache/.
|
# la source : le stream joue alors uniquement les fichiers déjà dans cache/.
|
||||||
RADIEO_NAVIDROME_URL=https://navidrome.example.org
|
RADIEO_SUBSONIC_URL=https://subsonic.example.org
|
||||||
RADIEO_NAVIDROME_USER=monuser
|
RADIEO_SUBSONIC_USER=monuser
|
||||||
RADIEO_NAVIDROME_PASSWORD=monmotdepasse
|
RADIEO_SUBSONIC_PASSWORD=monmotdepasse
|
||||||
# Nom OU identifiant de la playlist à diffuser.
|
# Nom OU identifiant de la playlist à diffuser.
|
||||||
RADIEO_NAVIDROME_PLAYLIST=Radio
|
RADIEO_SUBSONIC_PLAYLIST=Radio
|
||||||
|
|
||||||
# --- Source yt-dlp ---
|
# --- Source yt-dlp ---
|
||||||
# La liste d'URL se met dans config/urls.txt (copier config/urls.txt.example).
|
# La liste d'URL se met dans config/urls.txt (copier config/urls.txt.example).
|
||||||
|
|
@ -18,12 +18,12 @@ RADIEO_NAVIDROME_PLAYLIST=Radio
|
||||||
# Feed Atom de recommandations. URL http(s) du feed de syndication, ou chemin
|
# Feed Atom de recommandations. URL http(s) du feed de syndication, ou chemin
|
||||||
# local sous /config (ex. /config/recommendations.xml) pour tester. Vide = off.
|
# local sous /config (ex. /config/recommendations.xml) pour tester. Vide = off.
|
||||||
# ListenBrainz ne fait que *nommer* des morceaux : chacun est résolu vers
|
# ListenBrainz ne fait que *nommer* des morceaux : chacun est résolu vers
|
||||||
# Navidrome puis, à défaut, yt-dlp.
|
# la bibliothèque OpenSubsonic puis, à défaut, yt-dlp.
|
||||||
RADIEO_LISTENBRAINZ_URL=https://listenbrainz.org/syndication-feed/user/monuser/recommendations/weekly-exploration
|
RADIEO_LISTENBRAINZ_URL=https://listenbrainz.org/syndication-feed/user/monuser/recommendations/weekly-exploration
|
||||||
|
|
||||||
# --- Dosage du mix entre sources (optionnel) ---
|
# --- Dosage du mix entre sources (optionnel) ---
|
||||||
# Poids relatifs de tirage de chaque source (0 désactive la source).
|
# Poids relatifs de tirage de chaque source (0 désactive la source).
|
||||||
RADIEO_WEIGHT_NAVIDROME=3
|
RADIEO_WEIGHT_SUBSONIC=3
|
||||||
RADIEO_WEIGHT_YTDLP=1
|
RADIEO_WEIGHT_YTDLP=1
|
||||||
RADIEO_WEIGHT_LISTENBRAINZ=2
|
RADIEO_WEIGHT_LISTENBRAINZ=2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ services:
|
||||||
- RADIEO_CACHE_DIR=/cache
|
- RADIEO_CACHE_DIR=/cache
|
||||||
- RADIEO_STATE_DIR=/state
|
- RADIEO_STATE_DIR=/state
|
||||||
- RADIEO_HTTP_PORT=8080
|
- RADIEO_HTTP_PORT=8080
|
||||||
# Source Navidrome / OpenSubsonic (voir .env / .env.example).
|
# Source OpenSubsonic (Navidrome, Gonic, Airsonic… ; voir .env.example).
|
||||||
# Laisser vide désactive la source : le stream joue alors son cache local.
|
# Laisser vide désactive la source : le stream joue alors son cache local.
|
||||||
- RADIEO_NAVIDROME_URL=${RADIEO_NAVIDROME_URL:-}
|
- RADIEO_SUBSONIC_URL=${RADIEO_SUBSONIC_URL:-}
|
||||||
- RADIEO_NAVIDROME_USER=${RADIEO_NAVIDROME_USER:-}
|
- RADIEO_SUBSONIC_USER=${RADIEO_SUBSONIC_USER:-}
|
||||||
- RADIEO_NAVIDROME_PASSWORD=${RADIEO_NAVIDROME_PASSWORD:-}
|
- RADIEO_SUBSONIC_PASSWORD=${RADIEO_SUBSONIC_PASSWORD:-}
|
||||||
- RADIEO_NAVIDROME_PLAYLIST=${RADIEO_NAVIDROME_PLAYLIST:-}
|
- RADIEO_SUBSONIC_PLAYLIST=${RADIEO_SUBSONIC_PLAYLIST:-}
|
||||||
- RADIEO_RETENTION_KEEP=${RADIEO_RETENTION_KEEP:-20}
|
- RADIEO_RETENTION_KEEP=${RADIEO_RETENTION_KEEP:-20}
|
||||||
# Source yt-dlp : liste d'URL dans config/urls.txt (créer depuis l'exemple).
|
# Source yt-dlp : liste d'URL dans config/urls.txt (créer depuis l'exemple).
|
||||||
- RADIEO_YTDLP_URLS_FILE=/config/urls.txt
|
- RADIEO_YTDLP_URLS_FILE=/config/urls.txt
|
||||||
|
|
@ -23,7 +23,7 @@ services:
|
||||||
# sous /config, ex. /config/recommendations.xml). Vide désactive la source.
|
# sous /config, ex. /config/recommendations.xml). Vide désactive la source.
|
||||||
- RADIEO_LISTENBRAINZ_URL=${RADIEO_LISTENBRAINZ_URL:-}
|
- RADIEO_LISTENBRAINZ_URL=${RADIEO_LISTENBRAINZ_URL:-}
|
||||||
# Dosage du mix entre les sources (0 désactive).
|
# Dosage du mix entre les sources (0 désactive).
|
||||||
- RADIEO_WEIGHT_NAVIDROME=${RADIEO_WEIGHT_NAVIDROME:-3}
|
- RADIEO_WEIGHT_SUBSONIC=${RADIEO_WEIGHT_SUBSONIC:-3}
|
||||||
- RADIEO_WEIGHT_YTDLP=${RADIEO_WEIGHT_YTDLP:-1}
|
- RADIEO_WEIGHT_YTDLP=${RADIEO_WEIGHT_YTDLP:-1}
|
||||||
- RADIEO_WEIGHT_LISTENBRAINZ=${RADIEO_WEIGHT_LISTENBRAINZ:-2}
|
- RADIEO_WEIGHT_LISTENBRAINZ=${RADIEO_WEIGHT_LISTENBRAINZ:-2}
|
||||||
# Canonicalizer MusicBrainz (identité MBID inter-sources ; sans clé).
|
# Canonicalizer MusicBrainz (identité MBID inter-sources ; sans clé).
|
||||||
|
|
|
||||||
|
|
@ -35,24 +35,24 @@ def _build_pipeline(db: Database, canonicalizer):
|
||||||
fetchers = {} # backend name -> fetcher
|
fetchers = {} # backend name -> fetcher
|
||||||
subsonic_client = None # reused by ListenBrainz for resolution
|
subsonic_client = None # reused by ListenBrainz for resolution
|
||||||
|
|
||||||
if config.NAVIDROME_ENABLED:
|
if config.SUBSONIC_ENABLED:
|
||||||
from .fetchers.subsonic import SubsonicFetcher
|
from .fetchers.subsonic import SubsonicFetcher
|
||||||
from .providers.navidrome import NavidromeProvider
|
from .providers.subsonic import SubsonicProvider
|
||||||
from .subsonic import SubsonicClient
|
from .subsonic import SubsonicClient
|
||||||
|
|
||||||
subsonic_client = SubsonicClient(
|
subsonic_client = SubsonicClient(
|
||||||
config.NAVIDROME_URL, config.NAVIDROME_USER, config.NAVIDROME_PASSWORD
|
config.SUBSONIC_URL, config.SUBSONIC_USER, config.SUBSONIC_PASSWORD
|
||||||
)
|
)
|
||||||
provider = NavidromeProvider(subsonic_client, config.NAVIDROME_PLAYLIST, db)
|
provider = SubsonicProvider(subsonic_client, config.SUBSONIC_PLAYLIST, db)
|
||||||
providers.append((provider, config.SOURCE_WEIGHTS.get("navidrome", 0)))
|
providers.append((provider, config.SOURCE_WEIGHTS.get("subsonic", 0)))
|
||||||
fetchers["subsonic"] = SubsonicFetcher(subsonic_client, config.CACHE_DIR)
|
fetchers["subsonic"] = SubsonicFetcher(subsonic_client, config.CACHE_DIR)
|
||||||
log.info(
|
log.info(
|
||||||
"Navidrome source enabled (playlist=%r, weight=%d)",
|
"OpenSubsonic source enabled (playlist=%r, weight=%d)",
|
||||||
config.NAVIDROME_PLAYLIST,
|
config.SUBSONIC_PLAYLIST,
|
||||||
config.SOURCE_WEIGHTS.get("navidrome", 0),
|
config.SOURCE_WEIGHTS.get("subsonic", 0),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.warning("Navidrome not configured (RADIEO_NAVIDROME_*): source off.")
|
log.warning("OpenSubsonic not configured (RADIEO_SUBSONIC_*): source off.")
|
||||||
|
|
||||||
if config.SOURCE_WEIGHTS.get("ytdlp", 0) > 0 and config.YTDLP_URLS_FILE.exists():
|
if config.SOURCE_WEIGHTS.get("ytdlp", 0) > 0 and config.YTDLP_URLS_FILE.exists():
|
||||||
from .fetchers.ytdlp import YtdlpFetcher
|
from .fetchers.ytdlp import YtdlpFetcher
|
||||||
|
|
@ -77,7 +77,7 @@ def _build_pipeline(db: Database, canonicalizer):
|
||||||
from .providers.listenbrainz import ListenBrainzProvider
|
from .providers.listenbrainz import ListenBrainzProvider
|
||||||
|
|
||||||
# ListenBrainz has no backend of its own: it resolves each suggestion to
|
# ListenBrainz has no backend of its own: it resolves each suggestion to
|
||||||
# Navidrome then yt-dlp. Make sure the yt-dlp fetcher exists as a
|
# the Subsonic library then yt-dlp. Make sure the yt-dlp fetcher exists as a
|
||||||
# resolution target even when the yt-dlp *source* is off.
|
# resolution target even when the yt-dlp *source* is off.
|
||||||
if "ytdlp" not in fetchers:
|
if "ytdlp" not in fetchers:
|
||||||
from .fetchers.ytdlp import YtdlpFetcher
|
from .fetchers.ytdlp import YtdlpFetcher
|
||||||
|
|
@ -94,7 +94,7 @@ def _build_pipeline(db: Database, canonicalizer):
|
||||||
"ListenBrainz source enabled (feed=%s, weight=%d, resolve=%s)",
|
"ListenBrainz source enabled (feed=%s, weight=%d, resolve=%s)",
|
||||||
config.LISTENBRAINZ_URL,
|
config.LISTENBRAINZ_URL,
|
||||||
config.SOURCE_WEIGHTS["listenbrainz"],
|
config.SOURCE_WEIGHTS["listenbrainz"],
|
||||||
"navidrome+ytdlp" if subsonic_client else "ytdlp",
|
"subsonic+ytdlp" if subsonic_client else "ytdlp",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ HTTP_HOST = os.environ.get("RADIEO_HTTP_HOST", "0.0.0.0")
|
||||||
HTTP_PORT = int(os.environ.get("RADIEO_HTTP_PORT", "8080"))
|
HTTP_PORT = int(os.environ.get("RADIEO_HTTP_PORT", "8080"))
|
||||||
|
|
||||||
# Transport-level retries for transient connection errors on outgoing HTTP
|
# Transport-level retries for transient connection errors on outgoing HTTP
|
||||||
# (Navidrome, MusicBrainz, ListenBrainz). Applies to connect failures only.
|
# (OpenSubsonic, MusicBrainz, ListenBrainz). Applies to connect failures only.
|
||||||
HTTP_RETRIES = int(os.environ.get("RADIEO_HTTP_RETRIES", "2"))
|
HTTP_RETRIES = int(os.environ.get("RADIEO_HTTP_RETRIES", "2"))
|
||||||
|
|
||||||
# --- Prefetching / retention ---
|
# --- Prefetching / retention ---
|
||||||
|
|
@ -49,18 +49,18 @@ USER_AGENT = os.environ.get(
|
||||||
"RADIEO_USER_AGENT", "radieo/0.1 (personal music radio)"
|
"RADIEO_USER_AGENT", "radieo/0.1 (personal music radio)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Navidrome / OpenSubsonic source ---
|
# --- OpenSubsonic source (Navidrome, Gonic, Airsonic…) ---
|
||||||
# Left empty means the provider is disabled (the stream then plays its own
|
# Left empty means the provider is disabled (the stream then plays its own
|
||||||
# local-cache fallback). Credentials are expected to come from a .env file.
|
# local-cache fallback). Credentials are expected to come from a .env file.
|
||||||
NAVIDROME_URL = os.environ.get("RADIEO_NAVIDROME_URL", "").strip()
|
SUBSONIC_URL = os.environ.get("RADIEO_SUBSONIC_URL", "").strip()
|
||||||
NAVIDROME_USER = os.environ.get("RADIEO_NAVIDROME_USER", "").strip()
|
SUBSONIC_USER = os.environ.get("RADIEO_SUBSONIC_USER", "").strip()
|
||||||
NAVIDROME_PASSWORD = os.environ.get("RADIEO_NAVIDROME_PASSWORD", "")
|
SUBSONIC_PASSWORD = os.environ.get("RADIEO_SUBSONIC_PASSWORD", "")
|
||||||
NAVIDROME_PLAYLIST = os.environ.get("RADIEO_NAVIDROME_PLAYLIST", "").strip()
|
SUBSONIC_PLAYLIST = os.environ.get("RADIEO_SUBSONIC_PLAYLIST", "").strip()
|
||||||
# How often to reload the playlist contents, in seconds.
|
# How often to reload the playlist contents, in seconds.
|
||||||
PLAYLIST_REFRESH = float(os.environ.get("RADIEO_PLAYLIST_REFRESH", "300"))
|
PLAYLIST_REFRESH = float(os.environ.get("RADIEO_PLAYLIST_REFRESH", "300"))
|
||||||
|
|
||||||
NAVIDROME_ENABLED = bool(
|
SUBSONIC_ENABLED = bool(
|
||||||
NAVIDROME_URL and NAVIDROME_USER and NAVIDROME_PASSWORD and NAVIDROME_PLAYLIST
|
SUBSONIC_URL and SUBSONIC_USER and SUBSONIC_PASSWORD and SUBSONIC_PLAYLIST
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- yt-dlp source ---
|
# --- yt-dlp source ---
|
||||||
|
|
@ -74,7 +74,7 @@ YTDLP_REFRESH = float(os.environ.get("RADIEO_YTDLP_REFRESH", "300"))
|
||||||
# --- ListenBrainz suggestions source ---
|
# --- ListenBrainz suggestions source ---
|
||||||
# Atom recommendations feed. May be an http(s) URL (the real syndication feed)
|
# Atom recommendations feed. May be an http(s) URL (the real syndication feed)
|
||||||
# or a local file path (for testing with a saved sample). Empty disables it.
|
# or a local file path (for testing with a saved sample). Empty disables it.
|
||||||
# ListenBrainz only *names* tracks; each is resolved to Navidrome then yt-dlp.
|
# ListenBrainz only *names* tracks; each is resolved to the Subsonic library then yt-dlp.
|
||||||
LISTENBRAINZ_URL = os.environ.get("RADIEO_LISTENBRAINZ_URL", "").strip()
|
LISTENBRAINZ_URL = os.environ.get("RADIEO_LISTENBRAINZ_URL", "").strip()
|
||||||
# How often to reload the feed, in seconds (it refreshes weekly).
|
# How often to reload the feed, in seconds (it refreshes weekly).
|
||||||
LISTENBRAINZ_REFRESH = float(os.environ.get("RADIEO_LISTENBRAINZ_REFRESH", "3600"))
|
LISTENBRAINZ_REFRESH = float(os.environ.get("RADIEO_LISTENBRAINZ_REFRESH", "3600"))
|
||||||
|
|
@ -84,7 +84,7 @@ LISTENBRAINZ_ENABLED = bool(LISTENBRAINZ_URL)
|
||||||
# Relative odds of drawing from each source. Isolated here on purpose so the
|
# Relative odds of drawing from each source. Isolated here on purpose so the
|
||||||
# mix can later move to a config file. A weight of 0 disables a source.
|
# mix can later move to a config file. A weight of 0 disables a source.
|
||||||
SOURCE_WEIGHTS = {
|
SOURCE_WEIGHTS = {
|
||||||
"navidrome": int(os.environ.get("RADIEO_WEIGHT_NAVIDROME", "3")),
|
"subsonic": int(os.environ.get("RADIEO_WEIGHT_SUBSONIC", "3")),
|
||||||
"ytdlp": int(os.environ.get("RADIEO_WEIGHT_YTDLP", "1")),
|
"ytdlp": int(os.environ.get("RADIEO_WEIGHT_YTDLP", "1")),
|
||||||
"listenbrainz": int(os.environ.get("RADIEO_WEIGHT_LISTENBRAINZ", "2")),
|
"listenbrainz": int(os.environ.get("RADIEO_WEIGHT_LISTENBRAINZ", "2")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class Track:
|
||||||
locator: str # backend-specific: Subsonic song id, or a media URL
|
locator: str # backend-specific: Subsonic song id, or a media URL
|
||||||
artist: str
|
artist: str
|
||||||
title: str
|
title: str
|
||||||
origin: str # provider that produced it, e.g. "navidrome"
|
origin: str # provider that produced it, e.g. "subsonic"
|
||||||
mbid: str | None = None # filled by the Canonicalizer (milestone 5)
|
mbid: str | None = None # filled by the Canonicalizer (milestone 5)
|
||||||
source_ext: str | None = None # filename hint, e.g. "mp3", "flac"
|
source_ext: str | None = None # filename hint, e.g. "mp3", "flac"
|
||||||
source_url: str | None = None # container URL a track was picked from
|
source_url: str | None = None # container URL a track was picked from
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ Track directly, which gives a source-agnostic identity for free (no MusicBrainz
|
||||||
lookup needed).
|
lookup needed).
|
||||||
|
|
||||||
Unlike the other providers, ListenBrainz yields no audio of its own: it only
|
Unlike the other providers, ListenBrainz yields no audio of its own: it only
|
||||||
*names* tracks. Each pick is therefore **resolved** to a concrete backend —
|
*names* tracks. Each pick is therefore **resolved** to a concrete backend — the
|
||||||
Navidrome first (an OpenSubsonic ``search3``), then yt-dlp (a ``ytsearch1:``
|
OpenSubsonic library first (a ``search3``), then yt-dlp (a ``ytsearch1:``
|
||||||
query) as a fallback — so the download layer is unchanged.
|
query) as a fallback — so the download layer is unchanged.
|
||||||
|
|
||||||
``RADIEO_LISTENBRAINZ_URL`` may be an ``http(s)`` URL (the real syndication
|
``RADIEO_LISTENBRAINZ_URL`` may be an ``http(s)`` URL (the real syndication
|
||||||
|
|
@ -114,7 +114,7 @@ class ListenBrainzProvider:
|
||||||
def _resolve(self, rec: dict) -> Track | None:
|
def _resolve(self, rec: dict) -> Track | None:
|
||||||
artist, title, mbid = rec["artist"], rec["title"], rec["mbid"]
|
artist, title, mbid = rec["artist"], rec["title"], rec["mbid"]
|
||||||
|
|
||||||
song = self._search_navidrome(artist, title)
|
song = self._search_subsonic(artist, title)
|
||||||
if song is not None:
|
if song is not None:
|
||||||
return Track(
|
return Track(
|
||||||
backend="subsonic",
|
backend="subsonic",
|
||||||
|
|
@ -137,13 +137,13 @@ class ListenBrainzProvider:
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _search_navidrome(self, artist: str, title: str) -> dict | None:
|
def _search_subsonic(self, artist: str, title: str) -> dict | None:
|
||||||
if self._subsonic is None:
|
if self._subsonic is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
songs = self._subsonic.search_songs(title)
|
songs = self._subsonic.search_songs(title)
|
||||||
except (SubsonicError, httpx.HTTPError, OSError) as exc:
|
except (SubsonicError, httpx.HTTPError, OSError) as exc:
|
||||||
log.warning("Navidrome search failed for %s — %s: %s", artist, title, exc)
|
log.warning("Subsonic search failed for %s — %s: %s", artist, title, exc)
|
||||||
return None
|
return None
|
||||||
want_a, want_t = norm_name(artist), norm_name(title)
|
want_a, want_t = norm_name(artist), norm_name(title)
|
||||||
for s in songs:
|
for s in songs:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""NavidromeProvider: picks tracks from an OpenSubsonic playlist.
|
"""SubsonicProvider: picks tracks from an OpenSubsonic playlist.
|
||||||
|
|
||||||
|
Works with any OpenSubsonic-compatible server (Navidrome, Gonic, Airsonic…).
|
||||||
Emits ``subsonic`` tracks (locator = song id). The playlist is cached in
|
Emits ``subsonic`` tracks (locator = song id). The playlist is cached in
|
||||||
memory and refreshed periodically. A cheap local anti-repeat filters out songs
|
memory and refreshed periodically. A cheap local anti-repeat filters out songs
|
||||||
whose id was played recently; if that empties the pool (short playlist), the
|
whose id was played recently; if that empties the pool (short playlist), the
|
||||||
|
|
@ -18,11 +19,11 @@ from ..db import Database
|
||||||
from ..models import Track
|
from ..models import Track
|
||||||
from ..subsonic import SubsonicClient, SubsonicError
|
from ..subsonic import SubsonicClient, SubsonicError
|
||||||
|
|
||||||
log = logging.getLogger("radieo.provider.navidrome")
|
log = logging.getLogger("radieo.provider.subsonic")
|
||||||
|
|
||||||
|
|
||||||
class NavidromeProvider:
|
class SubsonicProvider:
|
||||||
name = "navidrome"
|
name = "subsonic"
|
||||||
|
|
||||||
def __init__(self, client: SubsonicClient, playlist_ref: str, db: Database):
|
def __init__(self, client: SubsonicClient, playlist_ref: str, db: Database):
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"""Minimal OpenSubsonic client (enough for Navidrome playback).
|
"""Minimal OpenSubsonic client (enough for playlist playback).
|
||||||
|
|
||||||
|
Works with any OpenSubsonic-compatible server (Navidrome, Gonic, Airsonic…).
|
||||||
Uses salted-token authentication (``t = md5(password + salt)``), the scheme
|
Uses salted-token authentication (``t = md5(password + salt)``), the scheme
|
||||||
recommended by the Subsonic API since 1.13.0 and supported by Navidrome.
|
recommended by the Subsonic API since 1.13.0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -105,7 +106,7 @@ class SubsonicClient:
|
||||||
def download(self, song_id: str, dest: Path, hint_ext: str | None = None) -> str:
|
def download(self, song_id: str, dest: Path, hint_ext: str | None = None) -> str:
|
||||||
"""Download a song to ``dest``; return the file extension used.
|
"""Download a song to ``dest``; return the file extension used.
|
||||||
|
|
||||||
``format=raw`` asks Navidrome for the original file (no transcoding),
|
``format=raw`` asks the server for the original file (no transcoding),
|
||||||
keeping quality and letting Liquidsoap decode it.
|
keeping quality and letting Liquidsoap decode it.
|
||||||
"""
|
"""
|
||||||
params = {**self._auth_params(), "id": song_id, "format": "raw"}
|
params = {**self._auth_params(), "id": song_id, "format": "raw"}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue