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

View File

@@ -40,15 +40,29 @@ def _hit_public(hit) -> dict:
def _job_public(job) -> dict:
return {"message": job.message, "job_id": job.id, "status": job.status,
body = {"message": job.message, "job_id": job.id, "status": job.status,
"hit": _hit_public(job.hit) if job.hit is not None else None,
"result": job.result, "error": job.error}
# Only a parked job carries choices; keep the key out of every other body.
if job.choices:
body["choices"] = job.choices
return body
@app.post("/fetch", dependencies=[Depends(require_key)])
def fetch(q: str = Query(..., min_length=1),
quality: str = Query("best"),
source: str = Query("auto")):
source: str = Query("auto"),
auto: bool = Query(False),
limit: int = Query(10, ge=1, le=25)):
"""The single entry point.
A URL is grabbed directly — no prompt. Free text goes through Lidarr: the
artist and 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. `auto=true` grabs the best release and never asks.
`source=youtube` skips Lidarr; `source=lidarr` drops the YouTube fallback.
"""
if quality not in mf.QUALITY_CHOICES:
raise HTTPException(status_code=422, detail=f"Invalid quality '{quality}'.")
@@ -70,7 +84,7 @@ def fetch(q: str = Query(..., min_length=1),
# Non-direct link (Spotify/Apple/...): resolve via Odesli, then run the
# normal Lidarr-first pick/dispatch with the exact YouTube track fallback.
try:
query, hits = mf.resolve_link_hits(q, 10)
query, hits = mf.resolve_link_hits(q, limit)
except mf.OdesliError:
raise HTTPException(status_code=422,
detail=f"Couldn't resolve {q}. Try the direct YouTube or SoundCloud link.")
@@ -83,9 +97,10 @@ def fetch(q: str = Query(..., min_length=1),
response = _job_public(job)
jobs.run_job(
job.id,
lambda: actions.perform_fetch(chosen, hits, quality, ROOT),
done_message=actions.done_message(chosen),
fail_message=actions.failed_message(chosen),
# A URL is a direct grab, so never park it for a choice.
lambda: actions.perform_fetch(chosen, hits, quality, ROOT, auto=True),
done_message=lambda res: actions.done_message(chosen, res),
fail_message=lambda e: actions.fail_text(chosen, e),
)
return response
@@ -93,7 +108,7 @@ def fetch(q: str = Query(..., min_length=1),
raise HTTPException(status_code=422, detail=f"Invalid source '{source}'.")
yt_first = source == "youtube"
hits = mf.build_combined_hits(q, 10, yt_first,
hits = mf.build_combined_hits(q, limit, yt_first,
lidarr_only=(source == "lidarr"),
yt_only=(source == "youtube"))
if not hits:
@@ -105,11 +120,38 @@ def fetch(q: str = Query(..., min_length=1),
job = jobs.create_job(hit=chosen, message=actions.started_message(chosen))
response = _job_public(job) # snapshot "queued" state before background thread starts
jobs.run_choice_job(
job.id,
lambda: actions.perform_fetch(chosen, hits, quality, ROOT, auto=auto,
allow_youtube=(source != "lidarr")),
choice_message=lambda res: actions.choice_message(chosen, res),
done_message=lambda res: actions.done_message(chosen, res),
fail_message=lambda e: actions.fail_text(chosen, e),
)
return response
@app.post("/choose", dependencies=[Depends(require_key)])
def choose(job_id: str = Query(..., min_length=1), n: int = Query(..., ge=1)):
"""Send the release the client picked to the download client. `n` is the
number of a choice from the same job's `choices` list."""
try:
job, release = jobs.take_choice(job_id, n)
except KeyError:
raise HTTPException(status_code=404, detail="No such job.")
except jobs.ChoiceError as e:
raise HTTPException(status_code=409, detail=str(e))
prev = job.result or {}
artist, album = prev.get("artist", ""), prev.get("album", "")
jobs.requeue(job, actions.grab_started_message(artist, album))
response = _job_public(job)
jobs.run_job(
job.id,
lambda: actions.perform_fetch(chosen, hits, quality, ROOT),
done_message=actions.done_message(chosen),
fail_message=actions.failed_message(chosen),
lambda: actions.perform_grab(release, artist, album),
done_message=actions.grab_message,
fail_message=lambda e: (str(e) if isinstance(e, actions.FetchError) and str(e)
else "Lidarr refused that release."),
)
return response