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>
278 lines
10 KiB
Markdown
278 lines
10 KiB
Markdown
# 🎵 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), with `yt-dlp` scraping 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+
|
|
- `requests`
|
|
- `ytmusicapi` (recommended — accurate YouTube Music metadata)
|
|
- `rich` (recommended — nicer picker table + bold keyword matching)
|
|
|
|
```bash
|
|
pip install requests ytmusicapi rich
|
|
```
|
|
|
|
> **Note:** if you hit `ModuleNotFoundError: No module named 'idna'` from
|
|
> `requests`, repair it with:
|
|
> ```bash
|
|
> 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) and `ffmpeg` (for `-x` extraction / embedding).
|
|
|
|
```bash
|
|
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. |
|
|
|
|
```bash
|
|
export LIDARR_API_KEY="your-lidarr-api-key"
|
|
```
|
|
|
|
---
|
|
|
|
## 🧑💻 Usage
|
|
|
|
```bash
|
|
./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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
./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
|
|
|
|
```text
|
|
<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_URL` is reachable.
|
|
- **Wrong album art from YouTube:** install `ytmusicapi` so MusicFetch can resolve proper YouTube Music URLs and metadata.
|
|
- **`yt-dlp` errors:** update with `yt-dlp -U`; ensure `ffmpeg` is installed for extraction/embedding.
|
|
- **`idna` import error:** `pip install --force-reinstall idna requests`.
|
|
- **Permission denied writing files:** ensure the output root exists and is writable (`-o`/`--root` or `MUSICFETCH_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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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").
|
|
|
|
1. **Shortcuts app → New Shortcut.**
|
|
2. Add **Ask for Input** → Input Type **Text**, prompt "What should I fetch?".
|
|
(Or use **Dictate Text** for fully spoken input.)
|
|
3. 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.)
|
|
4. Add **Get Contents of URL**:
|
|
- **URL:** the Text variable from step 3.
|
|
- **Method:** `POST`.
|
|
- **Headers:** add one — key `X-API-Key`, value your `MUSICFETCH_API_KEY`.
|
|
- **Request Body:** leave as is (the query is in the URL).
|
|
5. Add **Get Dictionary Value** → Get Value for **message** in **Contents of URL**.
|
|
6. Add **Speak Text** → the Dictionary Value. Siri reads back
|
|
"Found '…' … Downloading now."
|
|
7. (Optional) To confirm completion: add **Get Dictionary Value** for `job_id`,
|
|
**Wait** ~20 seconds, **Get Contents of URL** on
|
|
`https://mf.izebra.net/jobs/<job_id>` (same `X-API-Key` header), then
|
|
**Get Dictionary Value** `message` → **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
|