Playlists download each track to per-track artist folders (CLI + REST API, one job per playlist, done if >=1 track succeeds). Profile selection by name with env overrides (LIDARR_METADATA_PROFILE/LIDARR_QUALITY_PROFILE). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.6 KiB
YouTube Playlists + Lidarr Profile Hardening — Design
Date: 2026-06-08 Status: Approved
Context & Goal
Two additions to musicfetch (and its REST API):
-
Lidarr profile hardening.
add_artistcurrently hardcodesqualityProfileId=1andget_default_metadata_profile_id()returnsprofiles[0]["id"]— the first profile, arbitrarily. If a user's profile order differs, every added artist could silently get the wrong metadata profile (e.g. OST or None) or quality profile. Select by name with an env override instead. -
YouTube playlist support. A playlist URL should download each track as its own file, routed into per-track artist folders, via both the CLI and the REST API (one playlist = one job).
Decisions (confirmed with user)
- Playlists work in CLI + REST API; one playlist = one job (option B).
- Tracks land in per-track artist folders
<root>/<artist>/youtube/(option A), reusing the existing single-track path. - Partial failures: job is
doneif ≥1 track succeeded (message"Downloaded 12/14 tracks (2 failed)");failedonly if zero succeed. - Playlist expansion prefers ytmusicapi for YT Music playlists, falls back to
yt-dlp --flat-playlist; each track then downloads through the existingact_youtube(first-artist folders, tag overrides,music.youtubeURLs). - A
watch?v=X&list=YURL stays a single track (no surprise batch); only a pure playlist URL (/playlist?list=orlist=withoutv=) triggers a batch.
Architecture
Feature 1 — Profile hardening (musicfetch)
get_default_metadata_profile_id()→ look up/api/v1/metadataprofile, pick the profile whose name matches envLIDARR_METADATA_PROFILE(default"Standard", case-insensitive); fall back to the first profile's id, then1.- New
get_quality_profile_id()→/api/v1/qualityprofile, match envLIDARR_QUALITY_PROFILE(default"Any"); fall back to first id, then1. add_artistusesget_quality_profile_id()instead of literal1.
Feature 2 — Playlists
New units in musicfetch:
is_playlist_url(url) -> bool
expand_playlist(url) -> tuple[str, list[Hit]] # (playlist_title, track Hits)
download_playlist(url, root, quality, dry_run) -> tuple[int, int] # (ok, total)
is_playlist_url: true when the URL haslist=AND nov=param, or path contains/playlist. (Sowatch?v=...&list=...→ False.)expand_playlist: ifytmusicapiis available and the URL is a YT Music playlist, useYTMusic().get_playlist(playlist_id)→ tracks with artist/album/year/videoId. Otherwiseyt-dlp --flat-playlist -J <url>→ entries (title, uploader→artist, id). Map each toHit(source="youtube", kind="track", …, payload={"videoId": id}). Returns the playlist title + Hits. Empty/none →("", []).download_playlist: for each Hit callact_youtube(hit, root, quality, dry_run), catching per-track exceptions (count ok/total); returns(ok, total).handle_url(CLI): ifis_playlist_url→download_playlistand print"Downloaded N/M tracks"; else the existing single-URL download.
Feature 2 — REST API (server/)
POST /fetch currently treats q only as a search term. Add URL routing:
- In
server/app.pyfetch(): ifmf.is_url(q)→ create a download job (not a search). The job runs inserver/actions.py:is_playlist_url(q)→download_playlist(q, ROOT, quality, False)→ result{"ok": n, "total": m, "path": None, "lidarr_album_id": None}; message"Downloaded {n}/{m} tracks"(+({m-n} failed)when failures).- else single URL → reuse
handle_url-equivalent single download; message"Downloaded '<title>'".
- Response
hitfor a playlist:{"source":"youtube","kind":"playlist", "title": <playlist title>, "artist":"", "album":"", "year":""}. - Status:
doneifok ≥ 1(or single URL succeeded), elsefailed. actions.perform_fetch(search path) is unchanged; a newactions.perform_url_fetch(q, quality, root) -> dicthandles the URL branch, andstarted_message/done_messageget URL/playlist-aware variants.
Error Handling
- Per-track download failures in
download_playlistare caught and counted; the batch continues. A batch with zero successes → jobfailed. expand_playlistdegrades ytmusicapi → yt-dlp →("", []); an empty expansion yields afailedjob with message"No tracks found in playlist.".- Profile lookups already degrade to a sane fallback id on any HTTP error.
Testing
Unit (mock network / act_youtube, no real downloads):
is_playlist_url:/playlist?list=…→ True;watch?v=X&list=Y→ False;watch?v=X→ False; non-URL → False.get_default_metadata_profile_id/get_quality_profile_id: pick by env name; fall back to first when name absent; fall back to1on error.expand_playlist: maps ytmusicapi playlist JSON → Hits (title/artist/videoId); yt-dlp fallback path; empty →("", []).download_playlist: counts ok/total with one track'sact_youtuberaising.- API:
POST /fetchwith a playlist URL → job, batch message"Downloaded n/m…",donewhen ok≥1; single URL → single-download job; zero-success →failed.
Live check: a small real YT Music playlist (3-5 tracks) → each track lands in
<root>/<artist>/youtube/ with correct single-artist tags; job message reports
N/M.
Out of Scope (YAGNI)
Per-track job fan-out (option C), resume/skip-already-downloaded, playlist→Lidarr album matching, dedup across runs, progress streaming during a batch.