Skip to content

Navidrome (Beta): implement Navidrome media browser service with custom playlists and overview tab - #155

Open
lxBlazarxl wants to merge 34 commits into
retransmit:developmentfrom
lxBlazarxl:development
Open

Navidrome (Beta): implement Navidrome media browser service with custom playlists and overview tab #155
lxBlazarxl wants to merge 34 commits into
retransmit:developmentfrom
lxBlazarxl:development

Conversation

@lxBlazarxl

@lxBlazarxl lxBlazarxl commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Note

Navidrome Beta Status: The Navidrome service is currently in active Beta. Core browsing, metadata exploration, and custom playlist management are fully functional. In-app media playback functionality is currently in development and will be added soon in an upcoming release.

This PR introduces two major feature sets:

  1. Navidrome Service (Beta): Complete Subsonic API client integration, dynamic Material 3 Expressive styling, custom playlist management, and a dedicated Overview tab.
  2. Dashdot Dashboard Widget: Live server vitals monitor on the main dashboard with circular ring metrics.

Changes

1. Navidrome Service (Beta)

  • Custom Playlist Management:
    • Floating Action Button (FAB) on Playlists tab to create custom playlists with name, comment, and public/private toggles.
    • "Add to Playlist" picker modal available on track items and album tracklists.
    • Dynamic 2x2 collage artwork generated from playlist tracks.
    • Hero header with gradient overlay matching the album screen.
  • Overview Tab:
    • Placed as final tab [Albums, Artists, Playlists, Overview].
    • Displays Last Listened Artist, Last Listened Song, and a 3x3 Top Albums collage from local playback history.
    • Clean section dividers with dynamic Material 3 theme colors.
  • Subsonic Integration Endpoints:
    • Added getTopSongs, getSimilarSongs, getSimilarSongs2, and getNowPlaying.
  • Hard Metadata Refresh:
    • Integrated core_ui EasyRefresh across all four tabs to cascade cache invalidation to deep views (albums/artists).
  • Reverse Proxy Support:
    • Resolves base URLs and subpaths behind reverse proxies (Authelia, Nginx subpaths).

2. Dashdot Dashboard Widget

  • Live Vitals Widget: Integrated Dashdot monitoring widget on the main dashboard displaying real-time CPU, RAM, Storage, and Network utilization.
  • Circular Ring Metrics: Reusable DashdotRingMetrics component with animated circular progress indicators and dynamic M3 color tokens.
  • Service Integration: Supports direct server navigation, prevents duplicate dashboard cards, and cleanly separates Dashdot from Glances monitors.

Upcoming

  • In-app audio player, queue management, and playback controls for Navidrome.

Verification

  • flutter analyze verified.
  • Tested on device across real Navidrome and Dashdot instances.

The lock was written on a newer SDK than the 3.41.9 the workflows pin, so it
asked for meta 1.18.0, test 1.31.0, test_api 0.7.11 and test_core 0.6.17,
none of which 3.41.9 can supply. pub wants to downgrade all four, so
flutter pub get --enforce-lockfile refused and the iOS build failed in its
first step, before it compiled a line of the new service.

Regenerated on 3.41.9. The only change that survives is the workspace entry
for service_navidrome, which is the one this PR actually needs.
Navidrome answers 200 for everything. A healthy server, a rejected password
and a request missing its credentials are all 200 with JSON; only the status
inside the subsonic-response object tells them apart. The kind was registered
as reachable, which treats any answer as healthy, so the dashboard dot was
green whatever the credentials were.

The probe was not even sending credentials. AuthInterceptor had no arm for
this kind, so the user/password fell through to the catch-all that sends
nothing, and the shared probe and connection tester both went out anonymous.
The service module signed its own calls, which is why the screens worked
while the dot and Test connection did not.

So both halves: the interceptor now signs Subsonic requests the way the spec
asks, with the username, a fresh salt and an MD5 of the password with that
salt, and the health arm requires the envelope to actually say ok. Same shape
as the Unraid arm above it, and for the same reason.

Verified against Navidrome 0.64.0: a correct password is status ok, a wrong
one is status failed code 40, and no credentials at all is code 10, all three
at HTTP 200. The bodies in the test are those responses.

The salt is Random.secure rather than Random. It travels in the clear next to
the hash it salts, so a predictable one lets anyone who captures a request
precompute against it.
Two things, both in the client provider.

It built its Dio straight off the factory with nothing to close it, and it
watches the image epoch, so it rebuilds on every hard refresh. Each pull to
refresh therefore created a fresh Dio and abandoned the previous one with its
connection pool still open. instanceDioProvider, which the other twenty-odd
services use, closes on dispose.

And every read method parses the envelope optimistically, returning an empty
list when the key it wants is absent. That is right for an empty library and
wrong for a rejected credential, so a wrong password rendered as an empty
result on all four tabs with nothing logged and no error shown. Confirmed on
a device against a real server: green dot, Connected on both URLs, and four
empty tabs.

SubsonicErrorInterceptor turns a failed envelope into a DioException, so the
existing error handling has something to show. One interceptor rather than a
check in each of the twenty-two methods, which also keeps it out of the
parsing code. The guard before adding it matters because the Dio is shared.

Also restores the test suite to version control. It was added to .gitignore,
so CI never ran it and no reviewer could see it.
The music artwork resolution added to jellyfin_client.dart is a line-for-line
copy of the one in emby_client.dart: substituting the service name makes the
two diffs identical. Only the Emby path had a test, and because the logic is
duplicated rather than shared, that test protects none of the Jellyfin copy.

This covers the other half. The duplication itself is worth removing, but
picking a home for shared Emby and Jellyfin code is a wider decision than a
test belongs in.
@retransmit

Copy link
Copy Markdown
Owner

Went through this properly and tested it against a real Navidrome (0.64.0) on a device. The Navidrome work itself is good: zero analyze issues in the package, correct use of AtriumNetworkImage everywhere, no rootNavigator, and hand-written models are the right call given Subsonic's list-or-single-map quirk. All four tabs pull real data with artwork.

I pushed four commits into the branch rather than leaving a list of comments, since three of them were blocking.

CI was red before any of the review. The committed pubspec.lock pinned meta 1.18.0, test 1.31.0, test_api 0.7.11 and test_core 0.6.17, which the pinned Flutter 3.41.9 cannot supply, so flutter pub get --enforce-lockfile refused and the iOS build died in its first step. Your lock was regenerated on a newer SDK than we pin. Worth knowing that release.yml runs the same flag, so this would have broken the F-Droid reproducible build too, not just the iOS check. Regenerated on 3.41.9; the lock is now byte-identical to development, which is what it should have been, as workspace members never appear in the lock.

The health dot and Test connection were both meaningless for Navidrome. Subsonic answers 200 for everything, and the kind was registered as reachable, which treats any answer as healthy. On top of that AuthInterceptor had no arm for the kind, so the shared probe and the connection tester went out with no credentials at all. I put a wrong password on a working server and got a green Online dot plus "Local: Connected" and "External: Connected", while all four tabs sat empty. The interceptor now signs Subsonic requests properly and the health arm requires the envelope to say ok, the same shape as the Unraid arm right above it.

A wrong password looked like an empty library. Every read method returns an empty list when the key it wants is absent, which is right for an empty library and wrong for a rejected credential. Nothing was logged and no error was shown anywhere. Added a SubsonicErrorInterceptor that turns a failed envelope into a DioException, in one place rather than in each of the 22 methods.

The client Dio was never closed. navidromeClientProvider watches the image epoch, so it rebuilds on every hard refresh, and it built its Dio straight off the factory. Each pull to refresh abandoned a Dio with its connection pool open. Switched to instanceDioProvider like the other services.

Also switched the auth salt to Random.secure(). It travels in the clear next to the hash it salts.

Three things I left for you rather than deciding inside your PR:

The music artwork resolution in jellyfin_client.dart and emby_client.dart is 144 identical lines. Substituting the service name makes the two diffs byte-identical, and only the Emby path had a test, so half of it had no coverage. I added the missing Jellyfin test, but picking a home for shared Emby/Jellyfin code felt like your call.

_byKind in dashboard_board.dart now drops instances that share an endpoint. That is a shared helper used by every dashboard widget, and silently hiding a configured instance sits oddly with multi-instance being a headline feature. It also has no test.

getCoverArtUrl derives its salt from the instance id, so the token in every image URL never rotates. That is a reasonable trade for cache-key stability, but it means one captured URL is a permanently valid credential, and it is worth a comment saying so.

Smaller: navidrome_api.dart repeats the subsonic-response unwrap 18 times across 22 methods, and connection_tester.dart / new_services_render_test.dart are the two checklist items still untouched.

One process note: the Emby and Jellyfin changes alter the Activity tab for every existing media server user, in a PR titled "Navidrome (Beta)". Worth calling out in the release notes so it is not a surprise if something regresses.

Verification on my side: analyze is at the repo's existing 60 issues with none added, 139 app tests, 66 in core_networking and 6 new in service_navidrome all pass, and I ran it behind an nginx basic-auth proxy with a global Authorization header. 25 app requests went through the proxy authenticated, including 18 cover-art fetches, so reverse-proxy setups work including artwork. Release APKs also still build byte-identically across two clean builds, so the reproducible build is unaffected.

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