feat: monitor sweep, real MB browser, and worker-loop wiring

This commit is contained in:
Jonathan
2026-07-11 12:35:07 +02:00
parent 1f23aaa0e8
commit 133f6219ae
6 changed files with 118 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
from lyra_worker.browser import ArtistHit, ReleaseGroupInfo
class MusicBrainzBrowser:
"""Real MbBrowser via musicbrainzngs (imported lazily; not unit-tested offline)."""
def __init__(self, app_name: str = "Lyra", version: str = "0.1", contact: str = "lyra@localhost"):
self._app, self._version, self._contact = app_name, version, contact
def _ua(self):
import musicbrainzngs
musicbrainzngs.set_useragent(self._app, self._version, self._contact)
return musicbrainzngs
def search_artist(self, name: str) -> list[ArtistHit]:
mb = self._ua()
res = mb.search_artists(query=name, limit=8)
return [
ArtistHit(mbid=a["id"], name=a.get("name", ""), disambiguation=a.get("disambiguation", ""))
for a in res.get("artist-list", [])
]
def browse_release_groups(self, artist_mbid: str) -> list[ReleaseGroupInfo]:
mb = self._ua()
out: list[ReleaseGroupInfo] = []
offset = 0
while True:
res = mb.browse_release_groups(artist=artist_mbid, limit=100, offset=offset)
groups = res.get("release-group-list", [])
for g in groups:
out.append(
ReleaseGroupInfo(
rg_mbid=g["id"],
title=g.get("title", ""),
primary_type=g.get("primary-type", "") or "",
secondary_types=tuple(g.get("secondary-type-list", []) or ()),
first_release_date=g.get("first-release-date", "") or "",
)
)
offset += len(groups)
if len(groups) < 100 or offset >= int(res.get("release-group-count", offset)):
break
return out
+21 -1
View File
@@ -3,10 +3,12 @@ import time
from lyra_worker.claim import claim_next
from lyra_worker.config import get_config
from lyra_worker.db import wait_for_db
from lyra_worker.monitor import MonitorConfig, reconcile, sweep
from lyra_worker.pipeline import run_pipeline
from lyra_worker.registry import build_adapters, build_resolver, build_tagger
from lyra_worker.registry import build_adapters, build_browser, build_resolver, build_tagger
IDLE_SLEEP = 2.0
MONITOR_TICK_SECONDS = 60.0
def run_forever() -> None:
@@ -14,16 +16,34 @@ def run_forever() -> None:
adapters = build_adapters(get_config(conn))
resolver = build_resolver()
tagger = build_tagger()
browser = build_browser()
print(f"worker: {len(adapters)} adapter(s) enabled: {[a.name for a in adapters]}", flush=True)
print("worker: waiting for jobs", flush=True)
last_tick = 0.0
try:
while True:
mcfg = MonitorConfig.from_config(get_config(conn))
now = time.monotonic()
if mcfg.enabled and now - last_tick >= MONITOR_TICK_SECONDS:
try:
sweep(conn, browser, mcfg)
except Exception as e: # a monitor error must never kill the worker
print(f"worker: monitor sweep failed: {e}", flush=True)
conn.rollback()
last_tick = now
job_id = claim_next(conn)
if job_id is None:
time.sleep(IDLE_SLEEP)
continue
print(f"worker: claimed job {job_id}", flush=True)
run_pipeline(conn, job_id, adapters, resolver=resolver, tagger=tagger)
if mcfg.enabled:
try:
reconcile(conn, job_id, mcfg)
except Exception as e:
print(f"worker: reconcile failed for job {job_id}: {e}", flush=True)
conn.rollback()
print(f"worker: finished job {job_id}", flush=True)
finally:
conn.close()
+6
View File
@@ -172,3 +172,9 @@ def reconcile(conn: psycopg.Connection, job_id: str, cfg: MonitorConfig) -> None
(quality, new_state, rel_id),
)
conn.commit()
def sweep(conn: psycopg.Connection, browser: MbBrowser, cfg: MonitorConfig) -> None:
"""One monitor pass: discover new releases, then enqueue everything due."""
discover(conn, browser, cfg)
enqueue_due(conn, cfg)
+7
View File
@@ -1,3 +1,4 @@
from lyra_worker._mbbrowser import MusicBrainzBrowser
from lyra_worker._musicbrainz import MusicBrainzResolver
from lyra_worker._mutagen import MutagenTagger
from lyra_worker.adapters._slskd import SlskdClient
@@ -7,6 +8,7 @@ from lyra_worker.adapters.base import SourceAdapter
from lyra_worker.adapters.qobuz import QobuzAdapter
from lyra_worker.adapters.soulseek import SoulseekAdapter
from lyra_worker.adapters.youtube import YouTubeAdapter
from lyra_worker.browser import MbBrowser
from lyra_worker.resolver import MbResolver
from lyra_worker.tagger import Tagger
@@ -33,3 +35,8 @@ def build_resolver() -> MbResolver:
def build_tagger() -> Tagger:
"""The tagger that writes canonical metadata onto downloaded files."""
return MutagenTagger()
def build_browser() -> MbBrowser:
"""The MusicBrainz browser used by the background monitor."""
return MusicBrainzBrowser()