Skip to content

feat: expose enriched song metadata to plugins#1045

Open
JMcG1 wants to merge 4 commits into
got-feedBack:mainfrom
JMcG1:feature/plugin-metadata-api
Open

feat: expose enriched song metadata to plugins#1045
JMcG1 wants to merge 4 commits into
got-feedBack:mainfrom
JMcG1:feature/plugin-metadata-api

Conversation

@JMcG1

@JMcG1 JMcG1 commented Jul 25, 2026

Copy link
Copy Markdown

What

Adds a generic Plugin Metadata API: any plugin can now get album, year,
genre, and MusicBrainz identifiers (recording/release/artist MBID, ISRC)
for the currently-playing or any known song — data the server already
computes via its MusicBrainz/AcoustID enrichment pipeline, but that
never reached a plugin before.

Two delivery surfaces, one shared resolver:

  • The highway WebSocket's song_info frame gains one new optional key,
    metadata.
  • A new GET /api/song/{filename}/metadata REST route serves the same
    shape for a song that isn't currently playing.

Both call the same lib/plugin_metadata.py::plugin_metadata_for()
there is exactly one place this assembly happens, so the two surfaces
can't drift apart in shape or in the rules that gate MusicBrainz data.

Why this is generic, not Song-Background-Manager-specific

This started as an investigation into what SBM (a community plugin)
would need for album art/MusicBrainz lookups. The server already had
everything — playback metadata, an enrichment pipeline with confirmed
MusicBrainz matches — but exposed none of it to any plugin. Rather than
build a one-off channel for one plugin, this exposes the same
information generically: any current or future plugin (visualizers,
scrobblers, tagging tools, alternate art sources) gets it through the
same public surface. SBM itself is not modified by this PR.

Backwards compatibility

Purely additive:

  • No existing field, on any existing route or WS frame, is removed,
    renamed, or reinterpreted.
  • The only new surface is one optional metadata key on song_info
    and one new REST route.
  • The metadata object has a stable set of documented keys and uses
    consistent empty or null defaults, allowing consumers to read fields
    safely without checking whether each individual key exists.
  • An old plugin that never reads metadata behaves identically before
    and after this change. A new plugin against an old server just sees
    metadata as undefined/absent.

How it works

  • plugin_metadata_for(filename, *, base_metadata=None) reads the
    existing songs cache (MetadataDB.pack_fields) and the existing
    song_enrichment table (MetadataDB.get_enrichment) — the existing
    indexed MetadataDB lookups, no new tables, no new query shapes, and
    no network access anywhere in this path.
  • base_metadata lets a caller that already has a loaded Song (the
    WebSocket handler, mid-playback) pass its album/year straight
    from the sloppak manifest instead of waiting on the next library
    scan to populate the cache — resolved per field
    (base_metadata → songs cache → safe default), never all-or-nothing.
    The REST route has no loaded Song to draw on, so it stays
    cache-only, exactly as before this parameter existed. (Note: this
    means album/year can briefly differ between the two surfaces for
    a song that hasn't been scanned yet — documented in
    docs/PLUGIN_METADATA_API.md.)
  • MusicBrainz identifiers/ISRC are surfaced only when the song's
    enrichment match is matched or manual — a review/failed/
    unscanned row's candidate data is a proposal, not a confirmed
    identity, and is never leaked as if it were one. This mirrors the
    same gate routers/art.py's cover-art candidate assembly already
    applies.

Why no duplication

Before this PR, adding metadata to both the WS payload and a REST route
independently would mean the two could silently disagree (e.g. one
surface's gating logic for MusicBrainz identifiers drifting from the
other's over time). Routing both through one function makes that
structurally impossible rather than a matter of remembering to keep
two implementations in sync.

Testing

  • tests/test_plugin_metadata.py — pure unit tests against a real
    MetadataDB (no FastAPI, no network): unknown-song defaults, year
    coercion edge cases, enrichment gating (matched/manual/review/failed/
    unscanned), base_metadata resolution (overrides cache, blank/zero
    falls through, partial base_metadata still pulls missing fields from
    cache, enrichment unaffected either way), JSON round-trip.
  • tests/test_plugin_metadata_api.py — WS/REST integration tests:
    route precedence (the new /metadata route can't be shadowed by the
    existing bare GET /api/song/{filename} catch-all), missing-song
    handling, and confirmation that every pre-existing song_info field
    is unchanged by this addition.
  • Full existing WebSocket regression suite (test_highway_ws_authors,
    test_highway_ws_instrument_routing, test_highway_ws_notation,
    test_ws_highway_disconnect) passing with no regressions.

See docs/PLUGIN_METADATA_API.md for the full field reference and
docs/PLUGIN_METADATA_FINAL_VERIFICATION.md for the pre-merge
verification pass.

feedpak surface

  • This PR does not change how the app reads/writes feedpaks
    (manifest keys, pack files, folder layout) — it only reads
    fields the existing loader/scanner already parse.

Checklist

  • CHANGELOG.md [Unreleased] updated
  • Tests added/updated for new behaviour
  • Commits are DCO signed off (git commit -s)

Summary by CodeRabbit

  • New Features
    • Added a Plugin Metadata API for retrieving album, year, genre, ISRC, and MusicBrainz identifiers.
    • Metadata is available through the REST API and included optionally with song information delivered over WebSocket.
    • Responses use a stable structure with safe empty or null values when information is unavailable.
  • Documentation
    • Added comprehensive API reference, compatibility, versioning, error-handling, and performance guidance.
  • Tests
    • Added coverage for REST and WebSocket behavior, enrichment states, missing songs, and backward compatibility.

SonicHH added 2 commits July 25, 2026 00:51
Signed-off-by: Jaime McGovern <jaimemcgovern@outlook.com>
Signed-off-by: Jaime McGovern <jaimemcgovern@outlook.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@JMcG1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b21d59ea-c80c-485a-9a33-f8d0bd8e894a

📥 Commits

Reviewing files that changed from the base of the PR and between ad37cb6 and be3cd95.

📒 Files selected for processing (2)
  • docs/PLUGIN_METADATA_API.md
  • docs/PLUGIN_METADATA_FINAL_VERIFICATION.md
📝 Walkthrough

Walkthrough

The PR adds a versioned plugin metadata assembler backed by cached song and enrichment data, exposes it through REST and WebSocket APIs, forwards it to browser song state, and documents and tests the additive contract.

Changes

Plugin Metadata API

Layer / File(s) Summary
Metadata contract and assembly
lib/plugin_metadata.py, tests/test_plugin_metadata.py
Defines the versioned metadata shape, normalizes album/year/genre values, gates MusicBrainz and ISRC identifiers by enrichment state, and tests defaults, precedence, serialization, and enrichment behavior.
REST and WebSocket delivery
lib/routers/song.py, lib/routers/ws_highway.py, static/highway.js, tests/test_plugin_metadata_api.py
Adds the metadata REST route, includes metadata in song_info, forwards it into currentSong, and tests route precedence, REST responses, WebSocket responses, and additive compatibility.
API documentation and verification record
docs/PLUGIN_METADATA_API.md, docs/PLUGIN_METADATA_FINAL_VERIFICATION.md, CHANGELOG.md
Documents the metadata schema, access paths, compatibility rules, error behavior, enrichment handling, and recorded verification results.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant REST
  participant Highway
  participant Metadata
  participant SQLite
  Client->>REST: GET /api/song/{filename}/metadata
  REST->>Metadata: plugin_metadata_for(filename)
  Metadata->>SQLite: Read song fields and enrichment
  SQLite-->>Metadata: Cached metadata
  Metadata-->>REST: Metadata object
  REST-->>Client: JSON response
  Client->>Highway: Request song_info
  Highway->>Metadata: plugin_metadata_for(song path)
  Metadata->>SQLite: Read song fields and enrichment
  SQLite-->>Metadata: Cached metadata
  Metadata-->>Highway: Metadata object
  Highway-->>Client: song_info.metadata
Loading

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.43% 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
Title check ✅ Passed The title clearly summarizes the main change: exposing enriched song metadata to plugins.
Description check ✅ Passed The description matches the template, covering What, feedpak surface, and checklist items with relevant details.
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 unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 2

🤖 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 `@docs/PLUGIN_METADATA_API.md`:
- Around line 174-178: Update the old-server fallback description in the plugin
metadata documentation to state that currentSong.metadata is assigned null when
the server omits metadata, rather than being absent or undefined. Adjust the
optional-chaining example and feature-detection guidance to match this
null-valued contract.

In `@docs/PLUGIN_METADATA_FINAL_VERIFICATION.md`:
- Around line 7-13: Regenerate the verification record in the documented report,
replacing stale test counts and coverage claims with results from the current
tests/test_plugin_metadata.py, including its 23 collected cases and
base-metadata coverage. Update the diff-scope statement to accurately reflect
the current nine-file diff, and remove any unsupported harness, validation, or
readiness assertions before publishing the record.
🪄 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: bd05f420-f663-4396-bf69-92007027211d

📥 Commits

Reviewing files that changed from the base of the PR and between eef58c8 and ad37cb6.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • docs/PLUGIN_METADATA_API.md
  • docs/PLUGIN_METADATA_FINAL_VERIFICATION.md
  • lib/plugin_metadata.py
  • lib/routers/song.py
  • lib/routers/ws_highway.py
  • static/highway.js
  • tests/test_plugin_metadata.py
  • tests/test_plugin_metadata_api.py

Comment thread docs/PLUGIN_METADATA_API.md Outdated
Comment thread docs/PLUGIN_METADATA_FINAL_VERIFICATION.md Outdated
SonicHH added 2 commits July 25, 2026 02:56
…t undefined)

Addresses CodeRabbit review comment on PR got-feedBack#1045:
static/highway.js assigns metadata: msg.metadata ?? null, so an old
server's song_info frame (which has no metadata key at all) still
results in currentSong.metadata being null on the client, not
undefined. The optional-chaining claim was still correct (null?.album
also safely evaluates to undefined) — only the direct null/undefined
claim was wrong.

Signed-off-by: Jaime McGovern <jaimemcgovern@outlook.com>
Replace the stale staged-not-committed snapshot with the feature's
actual state: three signed commits on feature/plugin-metadata-api,
pushed and open as got-feedBack#1045.

- Test counts: 16 -> 23 items in test_plugin_metadata.py, reflecting
  the 5 base_metadata test functions (7 pytest items) added since the
  prior pass.
- Incorporate real pytest evidence (31/31 across both test files,
  21/21 WS regression suite) from the contributor's own machine,
  superseding the manual-harness-only evidence this record previously
  relied on exclusively.
- Diff scope: 3 files -> the current 9-file diff (adds CHANGELOG.md),
  matching git diff --stat dd1927e..HEAD.
- Remove the stale 'READY TO COMMIT UPSTREAM PR' / 'do not commit, do
  not push' framing, since both have already happened.

No code changes. Manual harness re-run fresh and git diff --check
confirmed clean before committing.

Signed-off-by: Jaime McGovern <jaimemcgovern@outlook.com>
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.

2 participants