Bulk metadata fetches trip YouTube's per-session rate limit ("This
content isn't available, try again later"), failing even single-worker
runs after a burst. Add --sleep-requests between extraction calls (and a
randomized --sleep-interval before downloads), default 1s, tunable via
--sleep / $YTDLP_SLEEP (0 disables). Applied to metadata, search, probe,
and download yt-dlp invocations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: Odesli's linksByPlatform frequently lacks youtube/youtubeMusic
(confirmed live for many Spotify tracks). resolve_link_hits only added a
YouTube hit when Odesli supplied one, so with no Lidarr match the hit list
was empty -> server 404 'No results found'.
Fix: always run a normal youtube_search (via build_combined_hits) for the
fallback; when Odesli DID return a link, insert its exact track as the first
YouTube hit. Lidarr-first ordering preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve any streaming link (Spotify/Apple/Tidal/...) via the Odesli/song.link
API, then run the existing Lidarr-first, exact-YouTube-fallback flow. YouTube
and SoundCloud links keep direct yt-dlp download; text queries unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review finding: bare endswith routed look-alike hosts to the direct yt-dlp
path. Match on a domain-label boundary and drop the redundant _DIRECT_HOSTS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bulk --repair on unauthenticated YouTube trips the bot-check (HTTP 429 "Sign
in to confirm you're not a bot"), after which every call fails until the IP
flag clears. Add cookie support so authenticated requests bypass it:
- --cookies FILE / --cookies-from-browser BROWSER (and $YTDLP_COOKIES /
$YTDLP_COOKIES_FROM_BROWSER for the API container), threaded into every
yt-dlp invocation (search, probe, download, repair metadata fetch).
- run_yt_dlp_get_metadata now logs yt-dlp's last stderr line (the actual 429 /
bot-check / network reason) instead of a bare exit code.
- Default --repair workers lowered 8 -> 4 (safe without cookies; raise with).
- compose: optional YTDLP_COOKIES env + commented cookies mount.
- README: how to obtain cookies (Chrome/Firefox, browser-read vs cookies.txt
export); gitignore cookies.txt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each repaired file is an independent yt-dlp metadata round-trip, so repair is
network-bound; run them concurrently via ThreadPoolExecutor. Adds --workers
(default 8) to cap concurrency and a progress line every 100 files. At ~50k
tracks this turns a ~day-long sequential run into hours. Lower --workers if
YouTube rate-limits (429/403).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of bad album/title tags: yt-dlp's --parse-metadata reads a
single-word FROM (matching field_to_template's ^[a-zA-Z_]+$) as a *field
name*, so literal one-word titles/albums like "Cochise" became "NA". Inject
literals via seed-then-replace into meta_<tag> instead (--parse-metadata to
create the field, --replace-in-metadata with literal args to set it), which
is immune to template parsing and also creates tags the source lacks.
- yt_download: literal-safe meta_artist/title/album; hit album no longer
clobbered by the Unknown-Album default; artist tag now created when missing.
- lidarr_search: connection/timeout errors surface via err() ("Lidarr
unreachable … falling back to YouTube") instead of silent dbg(), so the
YouTube fallback isn't mistaken for "no Lidarr match".
- Dockerfile: install deno (arch-aware) — the JS runtime yt-dlp needs for
YouTube; without it: "No supported JavaScript runtime" / HTTP 403.
- repair: treat NA/Unknown placeholders as bogus and overwrite title/artist
from source (was fill-missing-only); normalise literal "NA" album to
"Unknown Album"; rename bogus "NA [<id>]" filenames to the recovered title.
- README updated; .gitignore excludes server/log.txt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Downloads with no album (regular YouTube videos, fan edits, etc) left a blank
album, which trips up players like Plexamp. yt_download now appends
--parse-metadata "%(album|Unknown Album)s:%(meta_album)s" so the native/resolved
album is kept when present, else 'Unknown Album' is embedded. Applies to all
download paths (search, playlist, single URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--repair only reads metadata (never downloads), so pass
--extractor-args youtube:player_skip=js to yt-dlp. Keeps album/artist/year/title
but avoids the slow, throttle-prone nsig JS step (which crawls without a JS
runtime and trips YouTube rate-limiting during bulk runs). run_yt_dlp_get_metadata
gains an optional extra_args param; the download path is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repeatable -x/--exclude NAME skips any artist- or source-level folder whose name
matches (case-insensitive) when walking the library, so hand-curated folders like
/media/music/Unsorted or .../playlists are left untouched. Threaded through
_iter_source_files -> repair_library / retag_library_from_path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Offline re-tag of artist/title from the artist folder + filename: strips
(Official Video)/(Lyrics)-style decorations and trailing [id], and treats an
'Artist - Title' filename as authoritative (recovering the real artist for
music videos filed under a channel name). Overwrites artist/title only; leaves
album/year. Honors --dry-run.
Refactors the source-folder walk into _iter_source_files, shared by --repair
and --retag-from-path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--repair was clobbering good tags and erroring on real libraries:
- Validate the parsed id per source (YouTube 11-char, SoundCloud numeric) so
junk ids from bracketed descriptors ([Official Video]) are skipped, not queried.
- Skip files whose source returns no real music metadata (no album/year, e.g.
music videos) instead of overwriting clean tags with channel/decorated titles.
- Year from release info only (sane 1000-2100), never upload_date (which gave
wrong years for old songs and bogus values like 6577).
- album/year are authoritative; artist/title are fill-missing-only (no clobber).
Also: download_single now uses the first artist for the folder (matching the
search/playlist paths) so single-URL downloads stop creating multi-artist dirs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Walks <root>/<artist>/<source>/ (known yt-dlp source folders only; skips Lidarr
album dirs), re-queries each file's source by the [id] in its filename, and fixes
tags (album/year/artist/title) via mutagen. Honors --dry-run for preview. CLI-only
(not the REST API). Fixes downloads that landed with missing album / wrong year.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- probe_url() classifies any URL via yt-dlp (SoundCloud sets, Bandcamp albums,
etc); YouTube playlists still use ytmusicapi for richer metadata.
- Per-source folders <root>/<artist>/<extractor>/; non-YouTube tracks download
by their native URL (YouTube keeps the music.youtube album-art URL).
- Sparse-metadata playlist tracks route via yt-dlp output template so they land
under the real artist.
Live-verified: SoundCloud track + set, YouTube playlist regression.
SoundCloud sets (and similar) return flat-playlist entries without per-track
artist/title. When a track Hit has no artist, download via an output template
(-o <root>/%(artist,uploader,channel)s/<source>/...) so yt-dlp places the file
under the real artist instead of "Unknown Artist". yt_download gains an optional
outtmpl mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalize URL handling beyond YouTube to any yt-dlp-supported site
(SoundCloud, Bandcamp, etc), single tracks and playlists/sets/albums.
- probe_url(): one yt-dlp --flat-playlist probe classifies playlist vs track
and returns per-entry Hits; YouTube playlists still use ytmusicapi.
- _track_url(): YouTube tracks keep the music.youtube album-art URL; other
platforms download via their native entry URL (no more videoId reconstruction).
- Per-source folders: <root>/<artist>/<extractor>/ (soundcloud/bandcamp/youtube)
instead of hardcoded youtube; download_single derives source from metadata.
- download_hits() downloads pre-probed Hits; API probes once and passes hits
into the job closure. Replaces YouTube-only is_playlist_url/expand_playlist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Playlist URLs download each track to per-artist folders (CLI + REST API).
One playlist = one job; done if >=1 track succeeds ("Downloaded N/M tracks").
- REST API /fetch now routes URL/playlist queries to download jobs.
- Lidarr metadata/quality profiles selected by name with env overrides
(LIDARR_METADATA_PROFILE/LIDARR_QUALITY_PROFILE), no more position-luck.
Defensive access guards against download_single returning ok=True
without title/artist keys, avoiding a KeyError in the job worker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- FastAPI async job-based REST API wrapping musicfetch (X-API-Key auth,
Siri-friendly messages, dockerized for the Lidarr stack).
- Smarter Lidarr search: MusicBrainz track->album resolution + exact
mbid: lookup (prefers own-artist studio album), no fuzzy ranking.
- Bug fixes from live testing: single first-artist tag (no doubling).
Add musicbrainz_best_album() that resolves an artist+track pair to its
best studio album via the MusicBrainz search API, with a 1 req/sec
courtesy rate-limiter. Prefers plain studio albums over compilations,
singles, and live releases; falls back to any release group when no
studio album is found. Never raises — returns None on any failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop fuzzy difflib scoring: MusicBrainz resolves track->album release-group
MBID, Lidarr album/lookup?term=mbid:<id> returns the exact album. Live-verified
against the user's Lidarr.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>