mirror of
https://github.com/myronblair/kino-app
synced 2026-07-27 21:18:43 -05:00
Add filesystem Library Scan (movies/tv/music) and a full Music library
- 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.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user