feat: rework Lidarr library integration

This commit is contained in:
2026-10-04 20:28:24 -07:00
parent 880c2217b1
commit b3e6f66097
17 changed files with 1839 additions and 127 deletions

132
README.md
View File

@@ -98,6 +98,7 @@ export LIDARR_API_KEY="your-lidarr-api-key"
| `--yt-only` | Skip Lidarr. |
| `-o`, `--root PATH` | Output root folder (default `/media/music`). |
| `--search-all` | Search all albums when adding an artist to Lidarr. |
| `--pick-release` | Show Lidarr's indexer releases and choose one, instead of letting Lidarr grab the best automatically. |
| `--repair` | Re-tag existing downloads under `--root` from source metadata (see below). |
| `--workers N` | Parallel metadata fetches during `--repair` (default 4). |
| `--cookies FILE` | yt-dlp `cookies.txt` for authenticated YouTube (avoids bot-check / rate limits). |
@@ -300,25 +301,83 @@ serves plain HTTP on `6769`.
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| `GET` | `/health` | no | Liveness check. |
| `POST` | `/fetch?q=...` | yes | Grab top hit; returns a `job_id`. |
| `POST` | `/fetch?q=...` | yes | The one entry point. Returns a `job_id`. |
| `POST` | `/choose?job_id=...&n=...` | yes | Send the chosen release to the download client. |
| `GET` | `/jobs/{id}` | yes | Poll job status. |
`POST /fetch` decides what to do from `q`:
- **`q` is a URL** → grab it directly. No prompt. A direct YouTube or SoundCloud
link downloads at once; a Spotify or Apple Music link resolves through Odesli
first.
- **`q` is free text** → go through Lidarr. The artist and the album land in the
library, Lidarr runs an interactive search, and the job parks at
`awaiting_choice` with the releases. Pick one with `POST /choose`.
`POST /fetch` params: `q` (required), `quality` (`best,320,m4a,opus,flac`),
`source` (`auto,lidarr,youtube`).
`source` (`auto,lidarr,youtube`), `auto` (`false` by default — `true` grabs the
best release and never asks), `limit` (1–25, default 10).
- `source=youtube` skips Lidarr. The top YouTube hit downloads at once.
- `source=lidarr` drops the YouTube fallback. No release means `no_release`.
- `auto=true` never parks the job. A URL always behaves this way.
`POST /choose` params: `job_id` and `n` (both required). `n` is the number of a
choice from the same job.
Job `status` is one of `queued`, `running`, `awaiting_choice`, `done`, `failed`
or `expired`. A job at `awaiting_choice` carries a `choices` array and waits for
`POST /choose`. It expires after 15 minutes (`MUSICFETCH_CHOICE_TTL`).
Every `message` states what really happened. An artist add says so — it does not
claim a download. A Lidarr grab says the release went to the download client.
### curl examples
```bash
# Kick off a fetch
curl -X POST 'https://mf.izebra.net/fetch?q=Under%20My%20Skin' \
# A URL: grabbed directly, no prompt.
curl -X POST 'https://mf.izebra.net/fetch?q=https://youtu.be/abc123' \
-H 'X-API-Key: a-long-random-secret'
# -> {"message":"Found 'Under My Skin' ... Downloading now.","job_id":"a1b2c3","status":"queued","hit":{...}}
# -> {"message":"Fetching '...'. Downloading now.","job_id":"a1b2c3","status":"queued"}
# Poll the job
# Poll any job.
curl 'https://mf.izebra.net/jobs/a1b2c3' -H 'X-API-Key: a-long-random-secret'
# -> {"message":"Finished downloading ...","status":"done","result":{...}}
# -> {"message":"Downloaded '...'.","status":"done","result":{...}}
# Text, and take whatever Lidarr thinks is best.
curl -X POST 'https://mf.izebra.net/fetch?auto=true&q=Overgrown%20-%20Lay%20Cold' \
-H 'X-API-Key: a-long-random-secret'
```
### Choose the release yourself
```bash
# 1. Free text. Lidarr adds the artist only if it is missing, monitors the
# album, then runs an interactive search. That takes up to a minute, so the
# search is part of the job.
curl -X POST 'https://mf.izebra.net/fetch?q=Overgrown%20-%20Lay%20Cold' \
-H 'X-API-Key: a-long-random-secret'
# -> {"message":"Found the album 'Lay Cold' by Overgrown in Lidarr. Searching indexers now.",
# "job_id":"a1b2c3","status":"queued","hit":{...}}
# 2. Poll until the job asks you a question.
curl 'https://mf.izebra.net/jobs/a1b2c3' -H 'X-API-Key: a-long-random-secret'
# -> {"status":"awaiting_choice",
# "message":"Overgrown is already in Lidarr. I found 3 releases for 'Lay Cold' by Overgrown. Which one?",
# "choices":[{"n":1,"label":"FLAC, 312 MB, 24 seeders, Redacted",
# "pick_label":"1. FLAC, 312 MB, 24 seeders, Redacted",
# "quality":"FLAC","size_mb":312,
# "seeders":24,"indexer":"Redacted","rejected":false}, ...]}
# 3. Pick one. Lidarr sends that exact release to the download client.
curl -X POST 'https://mf.izebra.net/choose?job_id=a1b2c3&n=1' \
-H 'X-API-Key: a-long-random-secret'
# -> {"message":"Sending 'Lay Cold' by Overgrown to the download client.","status":"queued"}
```
A job never parks when no indexer has the album. It falls back to YouTube Music
instead, and says so.
### 🗣️ Siri Shortcuts integration
Make a shortcut that fetches music by voice ("Hey Siri, fetch music").
@@ -326,9 +385,10 @@ 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.)
3. Add **Text** action, set it to: `https://mf.izebra.net/fetch?auto=true&q=`
then insert the **Provided Input** variable at the end. (Shortcuts
URL-encodes query variables automatically.) `auto=true` keeps this shortcut
one-shot — without it, free text stops and asks which release you want.
4. Add **Get Contents of URL**:
- **URL:** the Text variable from step 3.
- **Method:** `POST`.
@@ -344,6 +404,58 @@ Make a shortcut that fetches music by voice ("Hey Siri, fetch music").
Rename the shortcut (e.g. "Fetch Music") — that phrase becomes the Siri trigger.
#### Shortcut 2: choose the release yourself
This shortcut lists the indexer releases and lets you pick one.
Shortcuts has no "exit loop" action and no **OR** in an **If**, so the poll uses
a `Keep` flag and one **is** test per status.
**Start the search**
1. **Ask for Input** → Text, prompt "What should I find?".
2. **Text**: `https://mf.izebra.net/fetch?q=` + the **Provided Input**.
3. **Get Contents of URL** → Method `POST`, header `X-API-Key`.
4. **Get Dictionary Value** `job_id` → **Set Variable** `JobID`.
5. **Set Variable** `Keep` to `1`.
**Wait for the question** (the interactive search takes up to 90 seconds)
6. **Repeat** 20 times, with this inside:
- **If** `Keep` **is** `1`:
- **Text**: `https://mf.izebra.net/jobs/` + `JobID`.
- **Get Contents of URL** → Method `GET`, header `X-API-Key`.
- **Set Variable** `Reply` to **Contents of URL**.
- **Get Dictionary Value** `status` from `Reply` → **Set Variable** `Status`.
- **Set Variable** `Keep` to `0`.
- **If** `Status` **is** `queued` → **Set Variable** `Keep` to `1`.
- **If** `Status` **is** `running` → **Set Variable** `Keep` to `1`.
- **If** `Keep` **is** `1` → **Wait** 5 seconds.
**Read the answer**
7. **Get Dictionary Value** `message` from `Reply` → **Speak Text**.
8. **If** `Status` **is not** `awaiting_choice` → **Stop Shortcut**. (The job
finished or failed on its own. Step 7 already said why.)
**Pick a release**
9. **Get Dictionary Value** `choices` from `Reply`.
10. **Repeat with Each** item in it:
- **Get Dictionary Value** `pick_label` → **Add to Variable** `Labels`.
11. **Choose from List** `Labels`. Each row reads
`1. FLAC, 312 MB, 24 seeders, Redacted`.
12. **Split Text** the chosen row by Custom separator `. ` → **Get Item 1 from
List**. That is the number `n`.
13. **Text**: `https://mf.izebra.net/choose?job_id=` + `JobID` + `&n=` +
the number from step 12.
14. **Get Contents of URL** → Method `POST`, header `X-API-Key`.
15. **Get Dictionary Value** `message` → **Speak Text**
("Sending 'Lay Cold' … to the download client.").
The choice list stays valid for 15 minutes. After that, `/choose` answers `409`
and the job reads `expired`.
---
## 🛠️ Contributing