Skip to content

MediaInfo: fetch titles, synopses and artwork from public metadata APIs - #37

Merged
vyrti merged 2 commits into
mainfrom
feat/mediainfo
Aug 13, 2026
Merged

MediaInfo: fetch titles, synopses and artwork from public metadata APIs#37
vyrti merged 2 commits into
mainfrom
feat/mediainfo

Conversation

@vyrti

@vyrti vyrti commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Why

VuIO indexes media purely from local sources. Symphonia reads audio tags, but video files get no metadata at allmedia/scanner.rs gates extraction on audio/, so a movie or episode is a filename with no title, synopsis, year or artwork. Cover art exists only for audio, is re-read from disk on every request, and is never cached.

ideas/ideas.txt:34 has named this for a while ("Online Metadata Providers Integration"); nothing for it existed in the codebase.

What

A MediaInfo section in the Admin tab: per-provider credential fields, a Fetch media info for entire library button, live progress with a cancel button, and a review list of uncertain matches. Results flow through to the dashboard, to DLNA ContentDirectory, and to /media/{id}/cover.

Ten providers. Five need no account and are on by default; five are listed but idle until you save a credential.

No account needed Needs your own credential
TVmaze, MusicBrainz (+ Cover Art Archive), Jikan, AniList, Kitsu TMDb, OMDb, Discogs, Last.fm, Genius

Design decisions worth reviewing

  • Credentials live in the secrets table, not config.toml. This is functional, not stylistic: under Docker the config is env-derived and POST /api/admin/config returns 409, so a token in the file would be unsettable in exactly the deployment most likely to need one. It also keeps tokens out of files people paste into bug reports. A test asserts the status endpoint never echoes a stored token back through any key.
  • Fetched records get their own mediainfo table, not media_tags. media_tags is cleared and rewritten on every rescan, because it holds what the file claims and the file is the authority on that. This holds what somebody else said, which no amount of re-reading the file reproduces. ON DELETE CASCADE still cleans up when the file goes. There's a test for it.
  • Uncertain matches are stored but never displayed. Searching TVmaze for "Arrival" returns the series Dead on Arrival; relabelling the film with it is worse than showing the filename. Anything below min_confidence is filtered out of browse and DIDL, and listed in the Admin tab instead.
  • Per-provider rate limiting. MusicBrainz enforces 1 req/s per IP server-side and requires a User-Agent identifying the client, so a large music library is slow by design. The job is cancellable and resumable.
  • reqwest 0.13 behind the default-on mediainfo feature. http_client.rs is untouched — it is deliberately cleartext with no TLS, redirects or name resolution, and all three are required here. --no-default-features builds clean with the feature off.
  • Schema v3. The migration only adds a table, so an upgraded database behaves exactly as before until someone presses Fetch.

Verified against the live APIs

A real fetch over a 4-file library:

[SubsPlease] Death Note - 12 [1080p].mkv     -> Death Note               art ✓
Breaking.Bad.S02E05.1080p.WEB-DL.x264.mkv    -> Breaking Bad — Breakage  art ✓
Arrival.2016.1080p.BluRay.x264-GRP.mkv       -> flagged at 5%, not shown
song1.mp3                                    -> no match

A SOAP Browse returns real titles, <dc:description>, <upnp:genre> and <upnp:albumArtURI>, and /media/{id}/cover returns actual JPEGs for video — that endpoint returned 404 for video before this change.

Two bugs the live run caught and this PR fixes: low-confidence matches were being displayed, and episode titles lost their series name ("Breakage" rather than "Breaking Bad — Breakage").

Known limitation

Non-anime movies have no free source. TVmaze is TV-only and Jikan/AniList/Kitsu are anime, so a film can only score a spurious match against the key-free set — which is why Arrival lands in the flagged list rather than being trusted. Movies work once a TMDb or OMDb key is saved. This is inherent to the free-provider set.

Testing

455 tests pass. New coverage: filename parser and confidence scorer, all ten provider response parsers against recorded JSON fixtures (no network in tests), repository round-trips, a v1→v3 migration assertion, rescan-survival, the credential-leak assertion, and endpoint tests via tower::oneshot.

cargo test                                                # 455 passed
cargo build --no-default-features --features dashboard    # mediainfo off, clean
cargo build --no-default-features                         # clean

vyrti added 2 commits August 13, 2026 13:11
…ata APIs

VuIO indexed media purely from local sources: symphonia read audio tags, and
video files got no metadata at all — the scanner gates extraction on
`audio/`, so a movie was a filename with no title, synopsis, year or artwork.
Cover art existed only for audio, was re-read from disk on every request, and
was never cached.

This adds a MediaInfo section to the dashboard's Admin tab with a button that
looks the whole library up against public metadata services, and feeds the
results through to the dashboard, to DLNA ContentDirectory, and to
/media/{id}/cover.

Ten providers. Five answer without an account and are on by default — TVmaze,
MusicBrainz (with Cover Art Archive for artwork), Jikan, AniList and Kitsu.
Five more are listed but idle until a credential is saved: TMDb, OMDb,
Discogs, Last.fm and Genius.

Notable decisions:

- Credentials live in the `secrets` table, not config.toml. Under Docker the
  configuration is built from environment variables and the admin API refuses
  to write the file, so a token kept there would be unsettable in exactly the
  deployment most likely to need one.
- Fetched records go in their own `mediainfo` table rather than `media_tags`,
  which is cleared and rewritten on every rescan. What a provider said is not
  re-derivable from the file, so it has to survive a scan; it still goes when
  the file does, via the cascade.
- Matches are scored 0-100 and only shown above the configured threshold.
  Weaker ones are stored and listed in the Admin tab for review rather than
  used, because relabelling a film called Arrival as "Dead on Arrival" is
  worse than showing the filename.
- Requests are paced per provider to each publisher's documented limit.
  MusicBrainz enforces one request per second per IP at the server, which
  makes it the slowest path by design rather than by accident.
- reqwest 0.13 is behind the default-on `mediainfo` feature. http_client.rs
  is left alone: it is deliberately cleartext with no TLS, redirects or name
  resolution, and all three are required here.

Schema goes to v3; the migration only adds a table, so an upgraded file
behaves exactly as before until someone presses Fetch.
@vyrti
vyrti merged commit 76c3a44 into main Aug 13, 2026
22 checks passed
@vyrti
vyrti deleted the feat/mediainfo branch August 13, 2026 10:42
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