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).
79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
"""Schemas for the inline track-download feature.
|
|
|
|
This is a fork-only addition. Requests are proxied to a yt-dlp-worker sidecar
|
|
on gnat which performs the actual yt-dlp call and post-download Lidarr/Plex
|
|
triggers. The library label (music | music-personal) is stamped server-side
|
|
from the MUSICSEERR_LIBRARY env var so that public musicseerr cannot drop
|
|
files into the personal library.
|
|
|
|
Search source: "youtube" (default; free-text yt-dlp search) or "spotify"
|
|
(Spotify Web API search; the worker resolves the chosen Spotify track to a
|
|
matching YouTube video at download time, transparently to the client).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from infrastructure.msgspec_fastapi import AppStruct
|
|
|
|
|
|
SearchSource = Literal["youtube", "spotify"]
|
|
|
|
|
|
class TrackDownloadSearchRequest(AppStruct):
|
|
query: str
|
|
limit: int = 5
|
|
source: SearchSource = "youtube"
|
|
|
|
|
|
class TrackDownloadCandidate(AppStruct):
|
|
# video_id is a YouTube video ID when source="youtube" and a Spotify
|
|
# track ID when source="spotify". The worker disambiguates by source.
|
|
video_id: str
|
|
url: str
|
|
title: str
|
|
source: SearchSource = "youtube"
|
|
channel: str | None = None
|
|
artist: str | None = None # populated for source="spotify"
|
|
album: str | None = None # populated for source="spotify"
|
|
duration_seconds: int | None = None
|
|
thumbnail_url: str | None = None
|
|
|
|
|
|
class TrackDownloadSearchResponse(AppStruct):
|
|
candidates: list[TrackDownloadCandidate]
|
|
|
|
|
|
class TrackDownloadRequest(AppStruct):
|
|
"""Inbound request from the frontend. The library param is intentionally
|
|
NOT included here — the backend stamps it from env config so the public
|
|
musicseerr instance cannot target the personal library."""
|
|
|
|
video_id: str
|
|
artist: str
|
|
album: str
|
|
track_title: str
|
|
source: SearchSource = "youtube"
|
|
target_duration_seconds: int | None = None # passed through for spotify→yt resolution
|
|
artist_mbid: str | None = None
|
|
track_position: int | None = None
|
|
disc_number: int | None = None
|
|
|
|
|
|
class TrackDownloadAccepted(AppStruct):
|
|
job_id: str
|
|
|
|
|
|
class TrackDownloadJobStatus(AppStruct):
|
|
id: str
|
|
status: str
|
|
artist: str
|
|
album: str
|
|
track_title: str
|
|
library: str
|
|
created_at: str
|
|
updated_at: str
|
|
file_path: str | None = None
|
|
error: str | None = None
|