mirror of
https://github.com/myronblair/kino-app
synced 2026-07-28 05:23:45 -05:00
514cbad4ab
- New admin Library Scan page walks the NAS mounts directly for movies, TV, and music not already in Radarr/Sonarr/local storage, and imports them with storage_type=scan (fully integrated: streamable, transcodable, listed in Admin) — Radarr/Sonarr import flows are untouched. - New Music section (nav tab, album-grid browse, per-album track list, persistent bottom player with queue/seek) backed by a Track model and range-request audio streaming endpoint. - MusicBrainz + Cover Art Archive integration (free, no API key) to fill in album art for scanned tracks, paced to their 1 req/sec rate limit. - Mounted the NAS music share into CT104 (new mp2 LXC mountpoint, CIFS read-only) alongside the existing video mount. - Optional Bluetooth/output-device picker on the player bar via the Audio Output Devices API (setSinkId) for already-paired speakers — Chrome/Edge only, since browsers can't pair new BT devices directly.
43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
"""MusicBrainz + Cover Art Archive clients — both free, no API key.
|
|
MusicBrainz rate limit is 1 req/sec for unauthenticated clients; callers must pace requests.
|
|
"""
|
|
import requests
|
|
from typing import Optional, Dict
|
|
|
|
_HEADERS = {"User-Agent": "StreamHoard/1.0 (self-hosted media library)"}
|
|
MB_BASE = "https://musicbrainz.org/ws/2"
|
|
COVER_ART_BASE = "https://coverartarchive.org"
|
|
|
|
|
|
def lookup_release(artist: str, album: str) -> Optional[Dict]:
|
|
"""Best-effort match for an artist+album against MusicBrainz's release index."""
|
|
query = f'release:"{album}" AND artist:"{artist}"'
|
|
try:
|
|
r = requests.get(f"{MB_BASE}/release/", params={"query": query, "fmt": "json", "limit": 1},
|
|
headers=_HEADERS, timeout=15)
|
|
r.raise_for_status()
|
|
releases = (r.json() or {}).get("releases") or []
|
|
except Exception:
|
|
return None
|
|
if not releases:
|
|
return None
|
|
rel = releases[0]
|
|
return {
|
|
"mbid": rel.get("id"),
|
|
"title": rel.get("title") or album,
|
|
"artist": (rel.get("artist-credit") or [{}])[0].get("name") or artist,
|
|
"year": (rel.get("date") or "")[:4] or None,
|
|
}
|
|
|
|
|
|
def cover_art_url(mbid: str) -> Optional[str]:
|
|
"""Returns the front cover URL if the Cover Art Archive has one for this release, else None."""
|
|
url = f"{COVER_ART_BASE}/release/{mbid}/front"
|
|
try:
|
|
r = requests.head(url, headers=_HEADERS, timeout=10, allow_redirects=True)
|
|
if r.status_code == 200:
|
|
return r.url
|
|
except Exception:
|
|
pass
|
|
return None
|