feat(server): route URL/playlist /fetch to download jobs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,3 +61,38 @@ def perform_fetch(chosen, hits: list, quality: str, root: str) -> dict:
|
|||||||
if not ok:
|
if not ok:
|
||||||
raise RuntimeError("Failed to add artist to Lidarr.")
|
raise RuntimeError("Failed to add artist to Lidarr.")
|
||||||
return {"path": None, "lidarr_album_id": None}
|
return {"path": None, "lidarr_album_id": None}
|
||||||
|
|
||||||
|
|
||||||
|
def url_started_message(kind: str, title: str = "") -> str:
|
||||||
|
if kind == "playlist":
|
||||||
|
return (f"Fetching playlist '{title}'. Downloading tracks now."
|
||||||
|
if title else "Fetching playlist. Downloading tracks now.")
|
||||||
|
return f"Fetching '{title}'. Downloading now." if title else "Fetching track. Downloading now."
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_done_message(result: dict) -> str:
|
||||||
|
ok, total = result.get("ok", 0), result.get("total", 0)
|
||||||
|
failed = total - ok
|
||||||
|
return f"Downloaded {ok}/{total} tracks" + (f" ({failed} failed)." if failed else ".")
|
||||||
|
|
||||||
|
|
||||||
|
def url_done_message(result: dict) -> str:
|
||||||
|
title = result.get("title", "")
|
||||||
|
return f"Downloaded '{title}'." if title else "Download complete."
|
||||||
|
|
||||||
|
|
||||||
|
def perform_url_fetch(url: str, quality: str, root: str) -> dict:
|
||||||
|
"""Download a URL (playlist -> batch, else single). Raises if nothing
|
||||||
|
downloaded so the job is marked failed."""
|
||||||
|
if mf.is_playlist_url(url):
|
||||||
|
ok, total, title = mf.download_playlist(url, root, quality, False)
|
||||||
|
if ok == 0:
|
||||||
|
raise RuntimeError(f"No tracks downloaded from playlist '{title}'." if title
|
||||||
|
else "No tracks downloaded from playlist.")
|
||||||
|
return {"kind": "playlist", "title": title, "ok": ok, "total": total,
|
||||||
|
"path": None, "lidarr_album_id": None}
|
||||||
|
info = mf.download_single(url, root, quality, False)
|
||||||
|
if not info.get("ok"):
|
||||||
|
raise RuntimeError("Download failed.")
|
||||||
|
return {"kind": "track", "title": info["title"], "artist": info["artist"],
|
||||||
|
"ok": 1, "total": 1, "path": None, "lidarr_album_id": None}
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ def fetch(q: str = Query(..., min_length=1),
|
|||||||
source: str = Query("auto")):
|
source: str = Query("auto")):
|
||||||
if quality not in mf.QUALITY_CHOICES:
|
if quality not in mf.QUALITY_CHOICES:
|
||||||
raise HTTPException(status_code=422, detail=f"Invalid quality '{quality}'.")
|
raise HTTPException(status_code=422, detail=f"Invalid quality '{quality}'.")
|
||||||
|
|
||||||
|
if mf.is_url(q):
|
||||||
|
kind = "playlist" if mf.is_playlist_url(q) else "track"
|
||||||
|
syn = mf.Hit(source="youtube", kind=kind, title="", artist="")
|
||||||
|
job = jobs.create_job(hit=syn, message=actions.url_started_message(kind))
|
||||||
|
response = _job_public(job)
|
||||||
|
done_msg = actions.playlist_done_message if kind == "playlist" else actions.url_done_message
|
||||||
|
jobs.run_job(
|
||||||
|
job.id,
|
||||||
|
lambda: actions.perform_url_fetch(q, quality, ROOT),
|
||||||
|
done_message=done_msg,
|
||||||
|
fail_message="Download failed.",
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
if source not in ("auto", "lidarr", "youtube"):
|
if source not in ("auto", "lidarr", "youtube"):
|
||||||
raise HTTPException(status_code=422, detail=f"Invalid source '{source}'.")
|
raise HTTPException(status_code=422, detail=f"Invalid source '{source}'.")
|
||||||
|
|
||||||
|
|||||||
65
tests/test_api_url.py
Normal file
65
tests/test_api_url.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
from server import jobs as jobs_mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clear_jobs():
|
||||||
|
jobs_mod.JOBS.clear()
|
||||||
|
yield
|
||||||
|
jobs_mod.JOBS.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_done(client, auth, job_id, timeout=2.0):
|
||||||
|
end = time.time() + timeout
|
||||||
|
while time.time() < end:
|
||||||
|
b = client.get(f"/jobs/{job_id}", headers=auth).json()
|
||||||
|
if b["status"] in ("done", "failed"):
|
||||||
|
return b
|
||||||
|
time.sleep(0.01)
|
||||||
|
raise AssertionError("job never finished")
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_url_batch_job(client, auth, monkeypatch):
|
||||||
|
monkeypatch.setattr("server.app.mf.download_playlist",
|
||||||
|
lambda url, root, quality, dry_run: (2, 3, "My Mix"))
|
||||||
|
r = client.post("/fetch", params={"q": "https://music.youtube.com/playlist?list=PLx"}, headers=auth)
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["status"] == "queued"
|
||||||
|
assert body["hit"]["kind"] == "playlist"
|
||||||
|
done = _wait_done(client, auth, body["job_id"])
|
||||||
|
assert done["status"] == "done"
|
||||||
|
assert "2/3" in done["message"]
|
||||||
|
assert done["result"]["ok"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_zero_success_fails(client, auth, monkeypatch):
|
||||||
|
monkeypatch.setattr("server.app.mf.download_playlist",
|
||||||
|
lambda url, root, quality, dry_run: (0, 3, "Dead Mix"))
|
||||||
|
body = client.post("/fetch", params={"q": "https://www.youtube.com/playlist?list=PLy"}, headers=auth).json()
|
||||||
|
done = _wait_done(client, auth, body["job_id"])
|
||||||
|
assert done["status"] == "failed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_video_url_download(client, auth, monkeypatch):
|
||||||
|
monkeypatch.setattr("server.app.mf.download_single",
|
||||||
|
lambda url, root, quality, dry_run: {"title": "Song", "artist": "A", "ok": True})
|
||||||
|
body = client.post("/fetch", params={"q": "https://music.youtube.com/watch?v=abc"}, headers=auth).json()
|
||||||
|
assert body["hit"]["kind"] == "track"
|
||||||
|
done = _wait_done(client, auth, body["job_id"])
|
||||||
|
assert done["status"] == "done"
|
||||||
|
assert "Song" in done["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_query_still_works(client, auth, monkeypatch):
|
||||||
|
from server import mf
|
||||||
|
hit = mf.Hit(source="youtube", kind="track", title="T", artist="A", payload={"videoId": "x"})
|
||||||
|
monkeypatch.setattr("server.app.mf.build_combined_hits",
|
||||||
|
lambda q, limit, yt_first, lidarr_only, yt_only: [hit])
|
||||||
|
monkeypatch.setattr("server.app.mf.pick", lambda hits, q, ni, yf: hits[0])
|
||||||
|
monkeypatch.setattr("server.app.actions.perform_fetch",
|
||||||
|
lambda chosen, hits, quality, root: {"path": "/x", "lidarr_album_id": None})
|
||||||
|
r = client.post("/fetch", params={"q": "Daft Punk - Discovery"}, headers=auth)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["status"] == "queued"
|
||||||
Reference in New Issue
Block a user