feat: rework Lidarr library integration
This commit is contained in:
132
README.md
132
README.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user