23c9125ad8
Backend CI / Lint (push) Waiting to run
Backend CI / Tests (push) Waiting to run
Squashes 26 incremental fork commits (Apr–May 2026) onto upstream main as a single
diff for cleaner cross-fork comparison. Original history preserved on the
pre-squash-backup tag locally.
Feature additions
─────────────────
• Inline single-track download via yt-dlp-worker proxy
New routes: POST /api/v1/track-download/search (source: youtube | spotify),
POST /api/v1/track-download, GET /api/v1/track-download/{id}. Frontend
TrackDownloadButton in album track list AND popular-songs row, with a per-button
source picker. Per-user rate limits live in the worker's SQLite store. On
completion the backend fires Lidarr RefreshArtist + Plex library refresh +
cache invalidation, and the popular-songs list auto-refreshes.
• Per-instance library pinning via MUSICSEERR_LIBRARY env
Backend stamps the library label server-side (music / music-personal /
music-shared); clients cannot override. Drives an instance-segregated
deployment of three musicseerr containers sharing one source tree.
• Lidarr-request flow (single-track requests via Lidarr indexers)
New routes: POST /api/v1/lidarr-request, GET /api/v1/lidarr-request/status.
Per-album asyncio.Lock keyed on album_mbid so rapid-clicks on the same album
serialize correctly. Cross-release track matcher with foreignTrackId →
foreignRecordingId → position+disc → exact-title → substring fallback chain,
evaluated per release (recording UUIDs frequently differ between album,
single, and deluxe edition releases of the same song). Flips
artist.monitored = True on request so Lidarr's WantedAlbums query reaches
the track. Full Lidarr-chain gate (artist AND album AND track) for the
status endpoint to avoid false-positive REQUESTED display. Persistent UI
state so button icons survive refresh and cross-album navigation.
• Privacy: show_now_playing toggle in Settings → Home
Default off. Plex /status/sessions returns active audio sessions across the
whole server with no library-section filter, so a shared instance leaks
every household member's listening activity. The merged store still emits
the user's local MusicSeerr playback bar; only server-derived sessions
(Plex / Jellyfin / Navidrome) are gated.
• Per-button visibility prefs for the track-row action cluster
Settings → Preferences → Download Options / Playback Buttons. Per-context
(popular_songs / album_page) force-off flags layered on top of the existing
source-availability gate.
• UX: wrap action cluster on mobile, hide LidarrRequestButton in tight
layouts, cross-album status-leak fix in AlbumTrackList ($effect keyed on
album.musicbrainz_id to rebuild lookup; map keyed by
"{albumMbid}:{position}:{disc}").
Test coverage
─────────────
Backend pytest: full suite green (2031/2031 as of squash). New: schema-default
tests for HomeSettings, lidarr_request_service cross-release matcher
regression test, singleton-registry expected-count bump to 59. Frontend
vitest: SettingsHome.svelte.spec covers new toggle, nowPlayingSessions
.svelte.spec covers the privacy gate (no fetch when off; fetches when on).
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""Schemas for the inline single-track Lidarr request feature.
|
|
|
|
Fork-only addition. Distinct from track_download.py (which proxies to
|
|
the yt-dlp-worker on gnat for YouTube/Spotify-resolved downloads).
|
|
|
|
This feature uses Lidarr's native indexer + download-client pipeline
|
|
(slskd / qBittorrent / etc.) to grab the requested track, leveraging
|
|
the track-monitored fork's PUT /track/monitor endpoint so that only
|
|
the requested track ends up on disk — sibling tracks in the same album
|
|
release are rejected at import by TrackMonitoredSpecification.
|
|
|
|
Requires lidarr_url to point at an instance running shaunrd0/Lidarr
|
|
(currently only lidarr-shared on gnat:8688).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from infrastructure.msgspec_fastapi import AppStruct
|
|
|
|
|
|
class LidarrRequestRequest(AppStruct):
|
|
"""Inbound request from the frontend.
|
|
|
|
album_mbid and track_mbid are required — they drive Lidarr's album
|
|
lookup/add and the per-track identification.
|
|
|
|
artist_mbid is optional and only used for diagnostic logging; Lidarr's
|
|
album lookup already returns the artist MBID server-side and that's
|
|
what we actually use for the add. Frontend callers without artist
|
|
context (TopSongsList, etc.) can leave it null.
|
|
"""
|
|
|
|
album_mbid: str
|
|
track_mbid: str
|
|
artist_mbid: str | None = None
|
|
# Fallback for matching when track_mbid alone doesn't disambiguate.
|
|
# Backend matcher tries them in order: foreignTrackId == track_mbid,
|
|
# foreignRecordingId == track_mbid, position+disc, then track_title.
|
|
# Frontend should send what it has — Popular Songs lists often lack
|
|
# position/disc, album-detail pages have all four.
|
|
track_position: int | None = None
|
|
disc_number: int | None = None
|
|
track_title: str | None = None
|
|
|
|
|
|
class LidarrRequestAccepted(AppStruct):
|
|
"""Returned 202 from the request endpoint.
|
|
|
|
`command_id` is Lidarr's command queue ID for the AlbumSearch — clients
|
|
can poll `/api/v1/command/{id}` against Lidarr directly if they want
|
|
progress, but for now the UI just shows success/error and lets the
|
|
download appear in the library when Lidarr finishes.
|
|
"""
|
|
|
|
status: str
|
|
album_id: int
|
|
album_title: str
|
|
track_id: int
|
|
track_title: str
|
|
other_tracks_unmonitored: int
|
|
command_id: int | None = None
|
|
note: str | None = None
|
|
|
|
|
|
class LidarrRequestTrackStatus(AppStruct):
|
|
"""Per-track status surfaced by GET /api/v1/lidarr-request/status.
|
|
|
|
The frontend uses this to render the LidarrRequestButton in the right
|
|
persistent state — checkmark for downloaded, hourglass for requested-
|
|
but-not-yet-downloaded, idle for not-requested. Without this, the
|
|
button only knows transient session state and forgets after refresh.
|
|
|
|
position + disc_number are returned alongside recording_mbid because
|
|
Lidarr's foreignRecordingId doesn't always equal MusicBrainz's
|
|
recording_id (Lidarr sometimes maps to a different variant). The
|
|
frontend prefers recording_mbid match but falls back to position+disc
|
|
when recording_mbid lookup misses.
|
|
"""
|
|
|
|
recording_mbid: str
|
|
position: int
|
|
disc_number: int
|
|
monitored: bool
|
|
has_file: bool
|
|
|
|
|
|
class LidarrRequestStatusResponse(AppStruct):
|
|
"""Returned by GET /api/v1/lidarr-request/status?album_mbid=X.
|
|
|
|
in_library = album exists in Lidarr at all. If false, no tracks are
|
|
requested (the button shows idle on every row). If true, walk `tracks`
|
|
to find the per-track status keyed by recording_mbid.
|
|
"""
|
|
|
|
in_library: bool
|
|
tracks: list[LidarrRequestTrackStatus]
|