fix: scan populates each followed artist's full discography (unmonitored)
A followed artist previously showed only the one owned album ("of 1")
because scan_library recorded just the resolved release. Now scan
browses each artist's full MusicBrainz release-group list once per
run and stores it as unmonitored MonitoredRelease rows, matching
web-follow behavior. The owned album keeps its monitored/fulfilled
state via ON CONFLICT DO NOTHING.
This commit is contained in:
@@ -25,8 +25,8 @@ def _set_config(conn, key: str, value: str) -> None:
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _run_scan(conn, resolver, probe, dest_root: str = DEST_ROOT) -> None:
|
||||
result = scan_library(conn, resolver, probe, dest_root)
|
||||
def _run_scan(conn, resolver, probe, browser, dest_root: str = DEST_ROOT) -> None:
|
||||
result = scan_library(conn, resolver, probe, browser, dest_root)
|
||||
_set_config(conn, "scan.result", f"imported {result.imported}, skipped {result.skipped}")
|
||||
_set_config(conn, "scan.requested", "false")
|
||||
print(f"worker: library scan done — {result.imported} imported, {result.skipped} skipped", flush=True)
|
||||
@@ -58,7 +58,7 @@ def run_forever() -> None:
|
||||
|
||||
if str(config.get("scan.requested", "")).strip().lower() in _TRUE:
|
||||
try:
|
||||
_run_scan(conn, resolver, probe)
|
||||
_run_scan(conn, resolver, probe, browser)
|
||||
except Exception as e: # a scan error must never kill the worker
|
||||
print(f"worker: library scan failed: {e}", flush=True)
|
||||
conn.rollback()
|
||||
|
||||
+43
-17
@@ -28,21 +28,41 @@ def _first_audio(album_path: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _upsert(conn, target, quality_class: int, fmt: str, path: str) -> bool:
|
||||
"""Record have + monitored + followed for a resolved album. Returns True if newly imported."""
|
||||
def _ensure_artist(conn, target) -> str | None:
|
||||
"""Upsert the WatchedArtist (by mbid) and return its id, or None if the target has no artist mbid."""
|
||||
if not target.artist_mbid:
|
||||
return None
|
||||
with conn.cursor() as cur:
|
||||
watched_id = None
|
||||
if target.artist_mbid:
|
||||
cur.execute(
|
||||
'INSERT INTO "WatchedArtist" (id, mbid, name, "autoMonitorFuture", "monitorFrom", "createdAt") '
|
||||
"VALUES (gen_random_uuid()::text, %s, %s, false, now(), now()) "
|
||||
'ON CONFLICT (mbid) DO NOTHING',
|
||||
(target.artist_mbid, target.artist),
|
||||
)
|
||||
cur.execute('SELECT id FROM "WatchedArtist" WHERE mbid = %s', (target.artist_mbid,))
|
||||
row = cur.fetchone()
|
||||
watched_id = row[0] if row else None
|
||||
cur.execute(
|
||||
'INSERT INTO "WatchedArtist" (id, mbid, name, "autoMonitorFuture", "monitorFrom", "createdAt") '
|
||||
"VALUES (gen_random_uuid()::text, %s, %s, false, now(), now()) "
|
||||
'ON CONFLICT (mbid) DO NOTHING',
|
||||
(target.artist_mbid, target.artist),
|
||||
)
|
||||
cur.execute('SELECT id FROM "WatchedArtist" WHERE mbid = %s', (target.artist_mbid,))
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def _populate_discography(conn, browser, watched_id, artist_mbid: str, artist_name: str) -> None:
|
||||
"""Store the artist's full release-group list as unmonitored rows (like a web follow)."""
|
||||
for rg in browser.browse_release_groups(artist_mbid):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'INSERT INTO "MonitoredRelease" (id, "watchedArtistId", "artistMbid", "artistName", '
|
||||
'"rgMbid", album, "primaryType", "secondaryTypes", "firstReleaseDate", monitored, state, "createdAt") '
|
||||
"VALUES (gen_random_uuid()::text, %s, %s, %s, %s, %s, %s, %s, %s, false, 'wanted', now()) "
|
||||
'ON CONFLICT ("rgMbid") DO NOTHING',
|
||||
(watched_id, artist_mbid, artist_name, rg.rg_mbid, rg.title, rg.primary_type or None,
|
||||
list(rg.secondary_types), rg.first_release_date or None),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _record_owned(conn, target, quality_class: int, fmt: str, path: str, watched_id) -> bool:
|
||||
"""Mark the owned release monitored+fulfilled and record the LibraryItem. Returns True if newly imported."""
|
||||
with conn.cursor() as cur:
|
||||
if target.rg_mbid:
|
||||
cur.execute(
|
||||
'INSERT INTO "MonitoredRelease" (id, "watchedArtistId", "artistMbid", "artistName", '
|
||||
@@ -55,7 +75,6 @@ def _upsert(conn, target, quality_class: int, fmt: str, path: str) -> bool:
|
||||
' "watchedArtistId" = COALESCE("MonitoredRelease"."watchedArtistId", EXCLUDED."watchedArtistId")',
|
||||
(watched_id, target.artist_mbid, target.artist, target.rg_mbid, target.album, quality_class),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
'INSERT INTO "LibraryItem" (id, "requestId", artist, album, path, source, format, '
|
||||
'"qualityClass", "importedAt") '
|
||||
@@ -68,11 +87,14 @@ def _upsert(conn, target, quality_class: int, fmt: str, path: str) -> bool:
|
||||
return newly
|
||||
|
||||
|
||||
def scan_library(conn: psycopg.Connection, resolver, probe: AudioProbe, dest_root: str = "/music") -> ScanResult:
|
||||
def scan_library(conn: psycopg.Connection, resolver, probe: AudioProbe, browser, dest_root: str = "/music") -> ScanResult:
|
||||
"""Walk `{dest_root}/{Artist}/{Album}/`, resolve each album against MusicBrainz, and record
|
||||
matched albums as have + monitored + followed. Unmatched or audio-less folders are skipped."""
|
||||
matched albums as have + monitored + followed. Also browses each followed artist's full
|
||||
discography (once per scan run) and stores it as unmonitored releases, matching web-follow
|
||||
behavior. Unmatched or audio-less folders are skipped."""
|
||||
imported = 0
|
||||
skipped = 0
|
||||
browsed: set[str] = set() # artists whose discography we've populated this run
|
||||
if not os.path.isdir(dest_root):
|
||||
return ScanResult(0, 0)
|
||||
for artist_name in sorted(os.listdir(dest_root)):
|
||||
@@ -95,6 +117,10 @@ def scan_library(conn: psycopg.Connection, resolver, probe: AudioProbe, dest_roo
|
||||
if target is None:
|
||||
skipped += 1
|
||||
continue
|
||||
if _upsert(conn, target, pr.quality_class, pr.fmt, album_path):
|
||||
watched_id = _ensure_artist(conn, target)
|
||||
if watched_id is not None and target.artist_mbid not in browsed:
|
||||
_populate_discography(conn, browser, watched_id, target.artist_mbid, target.artist)
|
||||
browsed.add(target.artist_mbid)
|
||||
if _record_owned(conn, target, pr.quality_class, pr.fmt, album_path, watched_id):
|
||||
imported += 1
|
||||
return ScanResult(imported, skipped)
|
||||
|
||||
Reference in New Issue
Block a user