Commit Graph

65 Commits

Author SHA1 Message Date
530b5b0406 typo in mutagen. fixed 2026-06-11 21:51:16 -07:00
0a4e6d474a added mutagen to requirements.txt 2026-06-11 21:48:33 -07:00
c0503187c5 Merge feat/repair-fast-meta: faster --repair via player_skip=js 2026-06-10 22:52:39 -07:00
a6aa469084 perf(repair): skip YouTube JS signature step when fetching tags
--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>
2026-06-10 22:52:39 -07:00
f071158c10 Merge feat/repair-exclude: -x/--exclude for repair/retag 2026-06-10 22:41:16 -07:00
c6bde6958a feat: -x/--exclude to skip folders during --repair/--retag-from-path
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>
2026-06-10 22:41:16 -07:00
7ea3ad2538 Merge feat/retag-from-path: offline tag recovery 2026-06-10 22:30:25 -07:00
9af7f91a25 feat: --retag-from-path to recover tags damaged by a prior --repair
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>
2026-06-10 22:30:25 -07:00
567d7578ad Merge fix/repair-music-videos: conservative --repair + first-artist single folder 2026-06-10 18:53:27 -07:00
c6e28a4f75 fix: harden --repair against music videos; first-artist folder for single URLs
--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>
2026-06-10 18:53:27 -07:00
1a81f64cc3 Merge feat/repair: re-tag existing downloads via --repair 2026-06-09 19:11:43 -07:00
fdc3cc84a5 feat: --repair flag to re-tag existing downloads from source metadata
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>
2026-06-09 19:11:43 -07:00
74eb63b243 Merge feat/multi-platform: any yt-dlp site, single tracks + playlists
- 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.
2026-06-09 06:56:18 -07:00
6730f1f141 fix: route sparse-metadata playlist tracks by yt-dlp's own metadata
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>
2026-06-09 06:55:56 -07:00
f103b6c253 feat: multi-platform URL & playlist support via yt-dlp probe
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>
2026-06-09 00:25:58 -07:00
7309ad3a29 Merge feat/playlists-profiles: YouTube playlists + Lidarr profile hardening
- 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.
2026-06-09 00:13:54 -07:00
90b9a01872 fix(server): use .get() for title/artist in perform_url_fetch result
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>
2026-06-09 00:00:45 -07:00
0f7ddd7697 feat(server): route URL/playlist /fetch to download jobs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:58:37 -07:00
ca36d2bb27 feat(server): re-export URL helpers; callable job done_message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:54:49 -07:00
aa9d177ed1 feat(youtube): playlist expansion + per-track download, success bools
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 23:49:26 -07:00
3ee49b17bd fix(lidarr): select metadata/quality profiles by name with env overrides
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:44:34 -07:00
6e6bec7a0d Plan playlists + profile hardening (5 TDD tasks)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 23:42:13 -07:00
a24c894c61 Add design spec: YouTube playlists + Lidarr profile hardening
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>
2026-06-08 23:38:06 -07:00
a424fbfd2f Merge feat/rest-api: REST API + smarter Lidarr matching
- 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).
2026-06-08 23:31:41 -07:00
b99e5eb9cb fix(lidarr): prefer own-artist studio album over various-artists comps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:30:15 -07:00
1661cb1742 refactor(lidarr): drop now-unused Timeout import 2026-06-08 23:24:59 -07:00
18f72a5626 feat(lidarr): exact MBID album lookup via MusicBrainz resolution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:22:11 -07:00
babbd84fda feat(lidarr): MusicBrainz track-to-album resolver
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>
2026-06-08 23:17:43 -07:00
b0e3ce6e6c feat(lidarr): add Artist - Track query splitter 2026-06-08 21:08:02 -07:00
45121dd807 Plan smarter Lidarr matching via exact MBID lookup
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>
2026-06-08 21:06:39 -07:00
6687a5a0fc Add design spec for smarter Lidarr matching
Scored best-first lidarr_search with MusicBrainz track->album resolution,
difflib scoring, preserved YouTube fallback. Fixes noninteractive API
picking junk (Pignickel) over the real album.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:51:15 -07:00
425a973d85 fix: write single first-artist tag, not doubled/multi-artist
Live end-to-end test surfaced two bugs in youtube tagging:
- `--replace-in-metadata artist .* NAME` matched twice and doubled the
  artist tag (e.g. "SLVMLORDSLVMLORD"). Anchor with ^.*$ to match once.
- Use only the first artist when several are present (SLVMLORD, not
  "SLVMLORD, Travis Bradley, ...") for both the embedded tag and the
  spoken/echoed API messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:39:03 -07:00
9984c162c6 fix(server): return {message} body for request validation errors
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:27:00 -07:00
eecf0836f7 fix(server): make .dockerignore effective at repo root, pin yt-dlp in requirements
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:23:37 -07:00
809de44e2e feat(server): Dockerfile and compose for the Lidarr stack
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:21:14 -07:00
5b6986e01c test(server): cover validation 422s and pick-None 404; tighten message assert
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:18:54 -07:00
d4c1b18e58 feat(server): /fetch and /jobs endpoints with async download jobs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:15:28 -07:00
49a45e6270 feat(server): FastAPI app with API-key auth and health check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:09:50 -07:00
257ed5e0a5 fix(server): announce track title not album in messages; cover error paths
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:07:09 -07:00
f4ffd23ed8 docs: REST API usage and Siri Shortcuts walkthrough
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:02:51 -07:00
9912eb48a4 feat(server): action dispatch with structured result and messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:02:18 -07:00
09a0d7e682 fix(server): harden job eviction and worker against missing job id
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:59:31 -07:00
35df01f08e feat(server): in-memory async job store with thread-pool worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:55:57 -07:00
c46ff2ff1a refactor(server): register loaded module in sys.modules, add __all__
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:54:37 -07:00
ad660afae3 feat(server): load musicfetch binary as importable module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:49:18 -07:00
11a57bfa67 Add implementation plan for MusicFetch REST API
TDD task breakdown: module loader, job store, action dispatch, FastAPI
auth/endpoints, Docker/compose, README + Siri Shortcuts walkthrough.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:46:13 -07:00
033bc00ccc Add design spec for MusicFetch REST API
Async job-based HTTP wrapper around the musicfetch binary, dockerized for the
Lidarr stack, X-API-Key auth, Siri-friendly human messages, port via
MUSICFETCH_PORT (default 6769).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:42:17 -07:00
9fd4c8585b Rewrite musicfetch as v2: dual-source search with interactive picker
Accept free-form queries (artist/album/title and combos) instead of strict
"Artist - Track". Search Lidarr and YouTube Music concurrently, present a
unified rich picker with bold keyword matching, and act on the chosen hit.

- Normalize results via a Hit dataclass across both sources
- Lidarr: /api/v1/search (album+artist) with album/artist lookup fallback
- YouTube: ytmusicapi for accurate metadata + music.youtube.com URLs,
  yt-dlp scrape fallback; tag overrides from the chosen hit
- Lidarr album pick adds artist+album monitored, runs interactive release
  search, and falls through to top YouTube hit when no indexer release exists
- argparse CLI: -n/--noninteractive, -s/--ytsearch, -d/--dry-run,
  -q/--quality, --limit, --lidarr-only/--yt-only, -o/--root, --search-all
- Config via LIDARR_URL / LIDARR_API_KEY / MUSICFETCH_ROOT env vars
- Update README; add .gitignore for __pycache__

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:30:47 -07:00
51871fd296 Additional tweaks to search_artist function. 2025-06-12 16:28:23 -07:00
0cfee8205c Command Line Arguments added to musicfetch script
--debug for debug mode
--search-all will search all music sources as soon as the artist is added to Lidarr. The default behavior is to not search all sources.
2025-06-12 16:24:44 -07:00