feat: track download progress on Job (migration + worker poller + API)

Add Job.downloadProgress (0.0-1.0), derived from a worker background
poller that counts audio files landing in staging / expected track
count, since streamrip only reports 0/1. No UI yet (slice B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-13 23:23:15 +02:00
parent 52e74f7709
commit 9d5e1ff58a
7 changed files with 120 additions and 22 deletions
+22
View File
@@ -0,0 +1,22 @@
import os
from lyra_worker.pipeline import _count_staged_audio
def test_count_staged_audio_counts_nested_audio_files_only(tmp_path):
nested = tmp_path / "Artist" / "Album"
nested.mkdir(parents=True)
(nested / "01.flac").write_bytes(b"")
(nested / "02.flac").write_bytes(b"")
(nested / "03.flac").write_bytes(b"")
(nested / "cover.jpg").write_bytes(b"")
assert _count_staged_audio(str(tmp_path)) == 3
def test_count_staged_audio_empty_dir_is_zero(tmp_path):
assert _count_staged_audio(str(tmp_path)) == 0
def test_count_staged_audio_missing_dir_is_zero():
assert _count_staged_audio("/nonexistent/path/for/sure") == 0
+9
View File
@@ -19,6 +19,12 @@ def _request_status(conn, job_id):
return cur.fetchone()[0]
def _download_progress(conn, job_id):
with conn.cursor() as cur:
cur.execute('SELECT "downloadProgress" FROM "Job" WHERE id = %s', (job_id,))
return cur.fetchone()[0]
def test_success_picks_best_source_and_imports(conn):
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
@@ -26,6 +32,7 @@ def test_success_picks_best_source_and_imports(conn):
assert _job_state(conn, job_id) == ("imported", "import")
assert _request_status(conn, job_id) == "completed"
assert _download_progress(conn, job_id) == 1.0 # a successful download completes at 100%
with conn.cursor() as cur:
cur.execute('SELECT source FROM "Candidate" WHERE "jobId" = %s AND chosen = true', (job_id,))
assert cur.fetchone()[0] == "qobuz" # highest quality won
@@ -41,6 +48,7 @@ def test_falls_through_when_best_download_fails(conn):
run_pipeline(conn, job_id, [FailingAdapter(), FakeSoulseek()], dest_root="/tmp/lib")
assert _job_state(conn, job_id) == ("imported", "import")
assert _download_progress(conn, job_id) == 1.0 # falls through, but still completes at 100%
with conn.cursor() as cur:
cur.execute('SELECT source FROM "Candidate" WHERE "jobId" = %s AND chosen = true', (job_id,))
assert cur.fetchone()[0] == "soulseek"
@@ -75,6 +83,7 @@ def test_all_downloads_fail_goes_to_needs_attention(conn):
run_pipeline(conn, job_id, [FailingAdapter()], dest_root="/tmp/lib")
assert _job_state(conn, job_id)[0] == "needs_attention"
assert _download_progress(conn, job_id) == 0.0 # never reached 1.0 — all attempts failed
def test_incomplete_download_goes_to_needs_attention(conn):