feat(server): action dispatch with structured result and messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
59
server/actions.py
Normal file
59
server/actions.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Glue between a chosen Hit and a side-effecting download. Mirrors musicfetch's
|
||||||
|
main() dispatch but returns a structured result dict and speakable messages."""
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from . import mf
|
||||||
|
|
||||||
|
|
||||||
|
def _source_label(hit) -> str:
|
||||||
|
return "YouTube Music" if hit.source == "youtube" else "Lidarr"
|
||||||
|
|
||||||
|
|
||||||
|
def _title(hit) -> str:
|
||||||
|
return hit.album or hit.title or hit.artist
|
||||||
|
|
||||||
|
|
||||||
|
def started_message(hit) -> str:
|
||||||
|
return f"Found '{_title(hit)}' by {hit.artist or 'unknown artist'} on {_source_label(hit)}. Downloading now."
|
||||||
|
|
||||||
|
|
||||||
|
def done_message(hit) -> str:
|
||||||
|
return f"Finished downloading '{_title(hit)}' by {hit.artist or 'unknown artist'}."
|
||||||
|
|
||||||
|
|
||||||
|
def failed_message(hit) -> str:
|
||||||
|
return f"Failed to download '{_title(hit)}' by {hit.artist or 'unknown artist'}."
|
||||||
|
|
||||||
|
|
||||||
|
def _yt_path(hit, root: str) -> str:
|
||||||
|
artist_dir = (hit.artist.split(",")[0].strip() if hit.artist else "") or "Unknown Artist"
|
||||||
|
return os.path.join(root, artist_dir, "youtube")
|
||||||
|
|
||||||
|
|
||||||
|
def _download_youtube(hit, quality: str, root: str) -> dict:
|
||||||
|
mf.act_youtube(hit, root, quality, False)
|
||||||
|
return {"path": _yt_path(hit, root), "lidarr_album_id": None}
|
||||||
|
|
||||||
|
|
||||||
|
def perform_fetch(chosen, hits: list, quality: str, root: str) -> dict:
|
||||||
|
"""Run the download for the chosen hit. Returns {"path", "lidarr_album_id"}.
|
||||||
|
Raises on unrecoverable failure (recorded by the job worker)."""
|
||||||
|
if chosen.source == "youtube":
|
||||||
|
return _download_youtube(chosen, quality, root)
|
||||||
|
|
||||||
|
if chosen.kind == "album":
|
||||||
|
handled = mf.act_lidarr_album(chosen, root, False, False)
|
||||||
|
if handled:
|
||||||
|
return {"path": None, "lidarr_album_id": chosen.payload.get("album", {}).get("id")}
|
||||||
|
# No indexer release -> fall through to the top YouTube hit, like the CLI.
|
||||||
|
yt = next((h for h in hits if h.source == "youtube"), None)
|
||||||
|
if yt is None:
|
||||||
|
raise RuntimeError("No Lidarr release and no YouTube fallback available.")
|
||||||
|
return _download_youtube(yt, quality, root)
|
||||||
|
|
||||||
|
# Lidarr artist pick.
|
||||||
|
ok = mf.act_lidarr_artist(chosen, root, False, False)
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError("Failed to add artist to Lidarr.")
|
||||||
|
return {"path": None, "lidarr_album_id": None}
|
||||||
60
tests/test_actions.py
Normal file
60
tests/test_actions.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from server import actions, mf
|
||||||
|
|
||||||
|
|
||||||
|
def make_yt_hit():
|
||||||
|
return mf.Hit(source="youtube", kind="track", title="Together",
|
||||||
|
artist="Avril Lavigne", album="Under My Skin", year="2004",
|
||||||
|
payload={"videoId": "abc"})
|
||||||
|
|
||||||
|
|
||||||
|
def make_lidarr_album_hit():
|
||||||
|
return mf.Hit(source="lidarr", kind="album", title="Under My Skin",
|
||||||
|
artist="Avril Lavigne", album="Under My Skin", year="2004",
|
||||||
|
payload={"album": {"id": 5, "title": "Under My Skin"}})
|
||||||
|
|
||||||
|
|
||||||
|
def test_started_message_mentions_source_and_title():
|
||||||
|
msg = actions.started_message(make_yt_hit())
|
||||||
|
assert "Under My Skin" in msg
|
||||||
|
assert "Avril Lavigne" in msg
|
||||||
|
assert "YouTube" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_message_mentions_title():
|
||||||
|
msg = actions.done_message(make_yt_hit())
|
||||||
|
assert "Under My Skin" in msg
|
||||||
|
assert "Avril Lavigne" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_youtube_calls_act_youtube(monkeypatch):
|
||||||
|
calls = {}
|
||||||
|
monkeypatch.setattr(mf, "act_youtube",
|
||||||
|
lambda hit, root, quality, dry_run: calls.update(hit=hit, root=root, quality=quality))
|
||||||
|
hit = make_yt_hit()
|
||||||
|
result = actions.perform_fetch(hit, [hit], quality="best", root="/media/music")
|
||||||
|
assert calls["quality"] == "best"
|
||||||
|
assert result["path"] == "/media/music/Avril Lavigne/youtube"
|
||||||
|
assert result["lidarr_album_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_lidarr_album_handled(monkeypatch):
|
||||||
|
monkeypatch.setattr(mf, "act_lidarr_album",
|
||||||
|
lambda hit, root, search_all, dry_run: True)
|
||||||
|
hit = make_lidarr_album_hit()
|
||||||
|
result = actions.perform_fetch(hit, [hit], quality="best", root="/media/music")
|
||||||
|
assert result["lidarr_album_id"] == 5
|
||||||
|
assert result["path"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_lidarr_album_fallsthrough_to_youtube(monkeypatch):
|
||||||
|
monkeypatch.setattr(mf, "act_lidarr_album",
|
||||||
|
lambda hit, root, search_all, dry_run: False)
|
||||||
|
yt_calls = {}
|
||||||
|
monkeypatch.setattr(mf, "act_youtube",
|
||||||
|
lambda hit, root, quality, dry_run: yt_calls.update(hit=hit))
|
||||||
|
lidarr_hit = make_lidarr_album_hit()
|
||||||
|
yt_hit = make_yt_hit()
|
||||||
|
result = actions.perform_fetch(lidarr_hit, [lidarr_hit, yt_hit],
|
||||||
|
quality="best", root="/media/music")
|
||||||
|
assert yt_calls["hit"] is yt_hit
|
||||||
|
assert result["path"] == "/media/music/Avril Lavigne/youtube"
|
||||||
Reference in New Issue
Block a user