Skip to content

refactor(server): move the metadata-enrichment subsystem into lib/enrichment.py (R3) - #861

Merged
byrongamatos merged 3 commits into
mainfrom
refactor/r3-enrichment-lib
Jul 11, 2026
Merged

refactor(server): move the metadata-enrichment subsystem into lib/enrichment.py (R3)#861
byrongamatos merged 3 commits into
mainfrom
refactor/r3-enrichment-lib

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

MusicBrainz / Cover-Art-Archive / AcoustID transport, the match-scorer glue, and the background enrichment worker (~930 lines, 61 defs) leave server.py as one cohesive unit → lib/enrichment.py.

Bodies verbatim; the only changes are seam reads:

  • meta_db / config_dir / sloppak_cache_dir / art_cache_dirappstate.<slot>
  • _song_pack_art_exists / _art_override_paths (stay in server.py for the art + delete routes) → appstate.<callable> — new slots, injected by reference
  • _env_flag → the existing identical env_compat.env_flag_compat; _artist_title_from_filename → imported from metadata_db (its home)
  • the User-Agent VERSION lookup: Path(__file__).parent.resolve().parents[1] (lib/enrichment.py → app root)

server.py drives the worker through the module (import enrichment; the routes + scan lifecycle call enrichment.X). Tests that faked the network on server (_mb_http_get, _enrich_network_enabled, _caa_http_get, …) now patch the same names on enrichment — the module attribute is resolved at call time, so one setattr reaches both the routes and the worker's internal callers. Acyclic: enrichment imports appstate/appconfig/dlc_paths/metadata_db/mb_match/acoustid_match/sloppak/loosefolder, never server.

server.py: 6,917 → 5,988 (−929).

Verification

  • Route table IDENTICAL (143); pyflakes clean bar the pre-existing File/safe_join/tuning_name/ET.
  • Full pytest 2400 passed — incl the 140 enrichment/art cases (the offline-safety "pytest never hits the network" contract + transport-error-pauses-pass-without-burning-attempts, all faking the network on enrichment).
  • test_packaging 44 passed (enrichment.py resolves under lib/); npm run lint 0; Codex 0 findings.
  • Boot smoke: /api/enrichment/status + POST /api/enrichment/kick serve from the new module.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Metadata enrichment continues with the same background workflow (MusicBrainz matching, audio fingerprint identification, and cover-art retrieval) but now has cleaner orchestration and synchronized progress/status reporting.
    • Enrichment status, cancellation, rematching, and refresh endpoints remain available and behave more consistently during long runs.
    • Cover-art fetching is more efficient with improved de-duplication, caching, and pruning behavior.
  • Documentation
    • Updated the changelog and size-exemption rationale to reflect the enrichment subsystem rework.
  • Bug Fixes
    • Strengthened offline/network error handling and safer validation around matching and artwork processing.
  • Tests
    • Updated the test suite to match the enrichment implementation change.

…ichment.py (R3)

MusicBrainz / Cover-Art-Archive / AcoustID transport, the match-scorer glue, and
the background enrichment worker (~930 lines, 61 defs) leave server.py as one
cohesive unit. Bodies are verbatim; the only changes are seam reads:

  meta_db / config_dir / sloppak_cache_dir / art_cache_dir -> appstate.<slot>
  _song_pack_art_exists / _art_override_paths (stay in server.py for the art +
    delete routes) -> appstate.<callable> (new seam slots, injected by reference)
  _env_flag -> env_compat.env_flag_compat (the existing identical helper)
  _artist_title_from_filename -> imported from metadata_db (its home)
  the User-Agent VERSION lookup: Path(__file__).parent ->
    Path(__file__).resolve().parents[1] (lib/enrichment.py -> app root)

server.py drives the worker through the module (import enrichment; the routes +
scan lifecycle call enrichment.X). Tests that faked the network on `server`
(_mb_http_get, _enrich_network_enabled, _caa_http_get, ...) now patch the same
names on `enrichment` — the module attribute is resolved at call time, so one
setattr reaches both the routes and the worker's internal callers. Acyclic:
enrichment imports appstate/appconfig/dlc_paths/metadata_db/mb_match/
acoustid_match/sloppak/loosefolder, never server.

server.py: 6,917 -> 5,988 (-929).

Verified: pyflakes clean (bar the pre-existing File/safe_join/tuning_name/ET);
route table IDENTICAL (143); full pytest 2400 passed (140 enrichment/art cases
incl the offline-safety + transport-error-pauses-pass contracts that fake the
network); test_packaging 44 passed (enrichment.py resolves under lib/); eslint 0.
Boot smoke: /api/enrichment/status + POST /kick serve from the new module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7574ede-a3bb-4924-8030-b11d2a1bbebc

📥 Commits

Reviewing files that changed from the base of the PR and between 9093230 and f0223ba.

📒 Files selected for processing (1)
  • tests/conftest.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/conftest.py

📝 Walkthrough

Walkthrough

The metadata-enrichment subsystem moves from server.py into lib/enrichment.py. Server endpoints and scan orchestration delegate to the new module, appstate exposes shared art seams, and tests patch enrichment-owned helpers directly.

Changes

Metadata enrichment extraction

Layer / File(s) Summary
Enrichment module foundation
lib/appstate.py, lib/enrichment.py, server.py
Adds enrichment-owned worker state, throttling, transport errors, art-cache helpers, and appstate-injected art seams.
Network and identity pipelines
lib/enrichment.py
Adds MusicBrainz, AcoustID, fingerprint, manifest identity, and alias lookup helpers.
Art, matching, and worker lifecycle
lib/enrichment.py
Adds Cover Art Archive caching, field filtering, matching precedence, background phases, retry handling, cancellation, and coalesced scheduling.
Server endpoint delegation
server.py
Removes inline enrichment implementation and delegates scan, enrichment, identify, artist-link, art, and gap-fill operations to enrichment.
Validation and documentation
tests/test_*.py, tests/conftest.py, CHANGELOG.md, docs/size-exemptions.md
Retargets tests to enrichment-owned seams, resets module-global test state, and documents the extraction.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant Enrichment
  participant Appstate
  participant ExternalAPIs
  Server->>Enrichment: kick enrichment pass
  Enrichment->>Appstate: read database and art seams
  Enrichment->>ExternalAPIs: fetch MusicBrainz, AcoustID, or CAA data
  ExternalAPIs-->>Enrichment: return enrichment data
  Enrichment->>Appstate: persist matches and art state
  Server->>Enrichment: read status or cancel worker
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: moving the metadata-enrichment subsystem from server.py into lib/enrichment.py.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/r3-enrichment-lib

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server.py (1)

2517-2574: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Enforce the upload cap before request.form()

max_part_size only applies to non-file fields; file parts still stream into Starlette’s temp file once parsing starts. The current Content-Length guard only covers honest clients, so a chunked multipart upload without that header can still consume temp storage before the later 413 in the read loop. Add an ASGI/proxy body limit or a true streaming size check ahead of form parsing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 2517 - 2574, Update api_enrichment_identify so the
request body is size-limited before request.form() begins parsing, including
chunked requests without Content-Length. Add or reuse an ASGI/proxy-level body
limit or equivalent streaming guard that rejects bodies exceeding the allowed
upload size before Starlette can spool file parts; retain the existing per-part
validation as a secondary safeguard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/enrichment.py`:
- Around line 722-758: Update _enrich_art_one to write downloaded cover data to
a temporary file in the same cache directory, then atomically replace the target
.jpg only after the write completes successfully. Ensure failed or partial
writes do not leave the final cache path behind, while preserving the existing
cache-hit and enrichment-state behavior.
- Around line 345-362: Update _acoustid_gate so the unavailable response
identifies the actual failed prerequisite instead of always claiming fpcalc is
missing. Distinguish missing/invalid AcoustID configuration or network-disabled
states from a missing fpcalc binary, while preserving the existing 412 response
for opt-in/key absence and the appropriate 503 status for
configured-but-unavailable dependencies.

In `@tests/test_context_menu_api.py`:
- Around line 89-91: Reset the enrichment worker state before this direct
_background_enrich() invocation so leaked _enrich_cancel and _enrich_status
values cannot short-circuit the test; add the reset to an appropriate per-test
fixture, or replace the direct call with _kick_enrich() if that is the
established state-safe entry point.

---

Outside diff comments:
In `@server.py`:
- Around line 2517-2574: Update api_enrichment_identify so the request body is
size-limited before request.form() begins parsing, including chunked requests
without Content-Length. Add or reuse an ASGI/proxy-level body limit or
equivalent streaming guard that rejects bodies exceeding the allowed upload size
before Starlette can spool file parts; retain the existing per-part validation
as a secondary safeguard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b562362-171e-4359-a4dd-fbf03d83871b

📥 Commits

Reviewing files that changed from the base of the PR and between 508829c and 5ce9f37.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/size-exemptions.md
  • lib/appstate.py
  • lib/enrichment.py
  • server.py
  • tests/test_art_candidates.py
  • tests/test_art_layer.py
  • tests/test_artist_page.py
  • tests/test_context_menu_api.py
  • tests/test_enrichment_plumbing.py
  • tests/test_field_overrides.py
  • tests/test_mb_enrichment.py
  • tests/test_scraper_options.py

Comment thread lib/enrichment.py
Comment thread lib/enrichment.py
Comment thread tests/test_context_menu_api.py
lib/enrichment.py now owns the worker, and it stays imported for the whole
session while the `server` fixtures pop-and-reimport `server` — so the cancel
Event / status dict / caches would leak across tests, and a stale `_enrich_cancel`
could short-circuit a later direct `_background_enrich()`. An autouse conftest
fixture clears that process-global state before each test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/conftest.py`:
- Around line 23-27: In the enrichment import setup around the try block,
replace the broad Exception handler with the specific import-related exception
raised when enrichment is unavailable, such as ModuleNotFoundError or
ImportError. Preserve yielding and returning for a genuinely unavailable module,
while allowing unrelated errors from enrichment to propagate and fail the test.
- Line 35: Update the test fixture cleanup around _caa_index_locks.clear() to
acquire _caa_index_locks_guard before mutating the shared dictionary, then
release it reliably after clearing. Preserve the existing fixture behavior while
ensuring cleanup is serialized with _caa_index_lock().
- Around line 12-37: Update the _reset_enrichment_state fixture to clear
enrichment._enrich_ua_cache alongside the existing process-global cache resets,
ensuring each test starts without a stale VERSION-derived User-Agent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: da776ad4-6556-4d2f-8541-b30858c350df

📥 Commits

Reviewing files that changed from the base of the PR and between 5ce9f37 and 9093230.

📒 Files selected for processing (1)
  • tests/conftest.py

Comment thread tests/conftest.py
Comment thread tests/conftest.py
Comment thread tests/conftest.py Outdated
Narrow the import guard to ImportError (not blind Exception, BLE001), and stop
clearing _caa_index_locks — it's guarded by _caa_index_locks_guard, so an
unlocked clear() would race a still-alive worker, and its per-release mutexes
carry no test state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos merged commit 165475d into main Jul 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant