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>
10 KiB
🎵 MusicFetch
MusicFetch is a smart command-line utility that finds music by searching Lidarr (your music collection manager) and YouTube Music at the same time, shows you the top hits in an interactive picker, and downloads/queues whatever you choose. It accepts:
- A free-form query: an artist, an album, a track title, or combos like
"Artist - Title"or"Artist - Album"(e.g."ODESZA - Bloom","Daft Punk","Discovery"). - A URL (e.g.
"https://music.youtube.com/watch?v=..."or a regular YouTube URL).
Lidarr is tried first by default. If you pick a Lidarr album but no indexer release is available, MusicFetch automatically falls through to the top YouTube hit. YouTube downloads prefer YouTube Music URLs so album art and tags are correct.
🚀 Features
- One unified picker showing the top hits from Lidarr and YouTube together, with matching keywords bolded.
- Lidarr-first flow: pick an album → adds artist+album (monitored) → interactive indexer search → falls through to YouTube only if no release is found.
- Accurate YouTube metadata via
ytmusicapi(real artist / album / year / album art), withyt-dlpscraping as a fallback. - Explicit tag overrides on download so files are tagged from the chosen hit, not from scraped titles.
- Non-interactive, YouTube-first, dry-run, quality, limit, and source-restriction flags.
📦 Dependencies & Installation
🐍 Python
- Python 3.10+
requestsytmusicapi(recommended — accurate YouTube Music metadata)rich(recommended — nicer picker table + bold keyword matching)
pip install requests ytmusicapi rich
Note: if you hit
ModuleNotFoundError: No module named 'idna'fromrequests, repair it with:pip install --force-reinstall idna requests
ytmusicapi and rich are optional — without them MusicFetch falls back to
yt-dlp search scraping and a plain ANSI picker.
📼 External Tools
yt-dlp(audio download/extraction) andffmpeg(for-xextraction / embedding).
pip install -U yt-dlp
sudo apt install ffmpeg # or your distro's equivalent
⚙️ Configuration
Set via environment variables:
| Variable | Default | Purpose |
|---|---|---|
LIDARR_API_KEY |
(required for Lidarr) | Lidarr API key. |
LIDARR_URL |
http://localhost:8686 |
Lidarr base URL. |
MUSICFETCH_ROOT |
/media/music |
Default output root folder. |
export LIDARR_API_KEY="your-lidarr-api-key"
🧑💻 Usage
./musicfetch [OPTIONS] QUERY...
Options
| Flag | Description |
|---|---|
-n, --noninteractive |
Auto-pick the top hit (no prompt). |
-s, --ytsearch |
Search/select YouTube first instead of Lidarr first. |
-d, --dry-run |
Show every action without executing it. |
-q, --quality {best,320,m4a,opus,flac} |
Audio quality/format (default best). |
--limit N |
Hits per source (default 10). |
--lidarr-only |
Skip YouTube. |
--yt-only |
Skip Lidarr. |
-o, --root PATH |
Output root folder (default /media/music). |
--search-all |
Search all albums when adding an artist to Lidarr. |
--repair |
Re-tag existing downloads under --root from source metadata (see below). |
--retag-from-path |
Offline: re-tag artist/title from folder + filename (see below). |
-x, --exclude NAME |
Folder under --root to skip during --repair/--retag-from-path (repeatable). |
--debug |
Verbose output. |
Examples
# Interactive: combined Lidarr + YouTube picker
./musicfetch "ODESZA - Bloom"
# Just an artist / just an album / just a title all work
./musicfetch "Daft Punk"
./musicfetch "Discovery"
# YouTube first, auto-pick top hit
./musicfetch -s -n "Daft Punk - Harder Better Faster Stronger"
# Dry run — see what would happen, change nothing
./musicfetch -d "ODESZA - Bloom"
# YouTube only, lossless preferred
./musicfetch --yt-only -q flac "Bonobo - Kerala"
# Download by URL (single track or playlist/set/album, any yt-dlp site)
./musicfetch "https://music.youtube.com/watch?v=xxxxxxxxxxx"
./musicfetch "https://soundcloud.com/artist/sets/my-mix"
🔧 Repair existing tags
--repair walks <root>/<artist>/<source>/ (the youtube/soundcloud/… download
folders — Lidarr album folders are skipped), re-fetches authoritative metadata for each
file using the [id] in its filename, and fixes tags. Useful when downloads landed with
missing album or wrong year.
It is deliberately conservative: it overwrites album and year (the usual
breakage), and fills in artist/title when they are missing or a known-bogus
placeholder (NA, Unknown Album, Unknown Artist — left behind by older buggy tagging) —
but it never overwrites a genuine existing artist/title with a channel name or decorated video
title. A bogus NA [<id>].<ext> filename is renamed to the recovered title, and a literal
NA album with no source album is normalised to Unknown Album.
It re-queries the source over the network, so run it occasionally, not constantly. Requires
mutagen (a yt-dlp dependency, usually already present). CLI-only — not exposed via the REST API.
# Preview what would change (writes nothing)
./musicfetch --repair -d
# Apply fixes under a specific root
./musicfetch --repair -o /media/music
--retag-from-path is an offline companion: it derives artist and title purely
from the folder name + filename (stripping (Official Video) / (Lyrics)-style decorations,
and treating an Artist - Title filename correctly), with no network. Use it to undo bad
tags — e.g. titles/artists clobbered by an earlier --repair on music videos. It overwrites
artist/title and leaves album/year alone.
./musicfetch --retag-from-path -d # preview
./musicfetch --retag-from-path -o /media/music
# Skip folders (e.g. hand-curated playlists you don't want re-tagged)
./musicfetch --repair -x Unsorted -x playlists
📁 Output Structure
<root>/
├── Artist Name/
│ ├── Album Name/ (managed by Lidarr)
│ ├── youtube/ (YouTube / YouTube Music downloads)
│ ├── soundcloud/ (SoundCloud downloads)
│ └── <source>/ (one folder per yt-dlp source)
❓ Troubleshooting
- No Lidarr hits / "LIDARR_API_KEY not set": export your key and confirm
LIDARR_URLis reachable. - Wrong album art from YouTube: install
ytmusicapiso MusicFetch can resolve proper YouTube Music URLs and metadata. yt-dlperrors: update withyt-dlp -U; ensureffmpegis installed for extraction/embedding.idnaimport error:pip install --force-reinstall idna requests.- Permission denied writing files: ensure the output root exists and is writable (
-o/--rootorMUSICFETCH_ROOT).
🌐 REST API (Docker)
Run MusicFetch as an authenticated HTTP service inside your Lidarr Docker stack.
A client POSTs a query; the server grabs the top hit non-interactively and runs
the download as a background job you can poll. Every response includes a
human-readable message (handy for Siri).
Configure & run
Set the network name in server/docker-compose.yml to your existing Lidarr
stack network, then:
export LIDARR_API_KEY="your-lidarr-key"
export MUSICFETCH_API_KEY="a-long-random-secret"
docker compose -f server/docker-compose.yml up -d --build
| Env var | Default | Purpose |
|---|---|---|
MUSICFETCH_API_KEY |
(required) | Shared secret clients send as X-API-Key. |
MUSICFETCH_PORT |
6769 |
Listen port. |
LIDARR_URL |
http://lidarr:8686 |
Lidarr base URL (stack network). |
LIDARR_API_KEY |
(required for Lidarr) | Lidarr API key. |
MUSICFETCH_ROOT |
/media/music |
Music output root (bind-mounted). |
TLS is expected to be handled by your upstream reverse proxy; the container
serves plain HTTP on 6769.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/health |
no | Liveness check. |
POST |
/fetch?q=... |
yes | Grab top hit; returns a job_id. |
GET |
/jobs/{id} |
yes | Poll job status. |
POST /fetch params: q (required), quality (best,320,m4a,opus,flac),
source (auto,lidarr,youtube).
curl examples
# Kick off a fetch
curl -X POST 'https://mf.izebra.net/fetch?q=Under%20My%20Skin' \
-H 'X-API-Key: a-long-random-secret'
# -> {"message":"Found 'Under My Skin' ... Downloading now.","job_id":"a1b2c3","status":"queued","hit":{...}}
# Poll the job
curl 'https://mf.izebra.net/jobs/a1b2c3' -H 'X-API-Key: a-long-random-secret'
# -> {"message":"Finished downloading ...","status":"done","result":{...}}
🗣️ Siri Shortcuts integration
Make a shortcut that fetches music by voice ("Hey Siri, fetch music").
- Shortcuts app → New Shortcut.
- Add Ask for Input → Input Type Text, prompt "What should I fetch?". (Or use Dictate Text for fully spoken input.)
- Add Text action, set it to:
https://mf.izebra.net/fetch?q=then insert the Provided Input variable at the end. (Shortcuts URL-encodes query variables automatically.) - Add Get Contents of URL:
- URL: the Text variable from step 3.
- Method:
POST. - Headers: add one — key
X-API-Key, value yourMUSICFETCH_API_KEY. - Request Body: leave as is (the query is in the URL).
- Add Get Dictionary Value → Get Value for message in Contents of URL.
- Add Speak Text → the Dictionary Value. Siri reads back "Found '…' … Downloading now."
- (Optional) To confirm completion: add Get Dictionary Value for
job_id, Wait ~20 seconds, Get Contents of URL onhttps://mf.izebra.net/jobs/<job_id>(sameX-API-Keyheader), then Get Dictionary Valuemessage→ Speak Text again.
Rename the shortcut (e.g. "Fetch Music") — that phrase becomes the Siri trigger.
🛠️ Contributing
PRs welcome. This script is middleware around Lidarr + yt-dlp, not a Lidarr replacement. Keep it a single bash-friendly executable.
📜 License
GPL V3.0