Skip to content

Add two-way favorites sync for Navidrome#48

Open
AshmitW wants to merge 8 commits into
PixelPlayerHQ:mainfrom
AshmitW:feature/navidrome-favorites-sync
Open

Add two-way favorites sync for Navidrome#48
AshmitW wants to merge 8 commits into
PixelPlayerHQ:mainfrom
AshmitW:feature/navidrome-favorites-sync

Conversation

@AshmitW

@AshmitW AshmitW commented Jul 19, 2026

Copy link
Copy Markdown

Hello! I love this app, it's been my daily player for a while now so thank you for working on it.
This is also my first ever PR to an open source project, so if I've gotten anything wrong process wise, please tell me and I'll fix it.

Problem

Favorites on Navidrome songs are currently local only. star()/unstar()
exist in NavidromeApiService but have no callers, there is no getStarred2
endpoint, and the unified library projection hardcodes isFavorite = false.
In practice: hearts set in the app never reach the server, stars set in the
Navidrome web UI never appear in the app, and a full sync resets the
songs.is_favorite flag on favorited Navidrome songs.

Changes

Push (app → server). MusicRepositoryImpl.setFavoriteStatus now records a
pending star/unstar op for Navidrome sourced songs in a new Room outbox table
(navidrome_pending_favorites, one row per song, repeated toggles collapse to
the latest intent). A network constrained NavidromeFavoritesPushWorker
(HiltWorker, exponential backoff) drains the queue: ops are deleted on server
acknowledgement and dropped after 8 failed attempts so an unreachable song
cannot block the queue. The local favorite is applied immediately (optimistic
update); the queue survives process death and cold starts.

Pull (server → app). New getStarred2 endpoint + syncStarredSongs() in
NavidromeRepository, called from syncAllPlaylistsAndSongs. Reconciliation
is a pure function (reconcileNavidromeFavorites, unit tested): server starred
state wins for Navidrome sourced songs, except songs with pending un pushed
local ops, where the local action wins. Only songs present in the unified
library gain favorites rows (existence check, chunked to respect SQLite's
variable limit).

Liked tab freshness. Favorites only refresh (a single getStarred2 call,
no library re fetch) fires when the Liked tab is opened (30s cooldown) and on
its pull-to-refresh (forced). Implemented as refreshStarredSongs() in the
repository with a thin PlayerViewModel delegate; no new UI surfaces, no
PlayerUiState collection. Full library sync cadence is unchanged.

Upgrade migration. On the first successful sync per login, preexisting
local favorites on Navidrome songs not starred server side are converted into
queued star pushes (flag stored in navidrome_prefs, reset on logout). Result:
server and app converge to the union, reconciliation can never delete a
favorite that predates this feature.

Bug fix. syncUnifiedLibrarySongsFromNavidrome now derives isFavorite
from the favorites table instead of hardcoding false. Bundled here because
reconciliation writes would otherwise be reverted on every sync.

Scope and safety

  • All queries are scoped to source_type = NAVIDROME; local and Jellyfin
    favorites are structurally unaffected.
  • A favorites-sync failure is caught and logged; it never fails the
    surrounding library sync.
  • Room v2 → v3: additive, idempotent migration (MIGRATION_2_3), schema
    exported; migration DDL matches the generated schema.
  • Behavior note for existing users: favorites made in app before this feature
    will appear as stars on their server after the first sync, that is the
    migration converging both sides, not a bug.
  • Jellyfin parity is intentionally out of scope; the reconciliation and outbox
    patterns are source agnostic if wanted later.

Checks

  • :app:compileDebugKotlin - BUILD SUCCESSFUL
  • :app:lintDebug - BUILD SUCCESSFUL
  • :app:testDebugUnitTest - 383 tests, 0 failures (14 new: reconciliation
    conflict cases, refresh cooldown, toggle push wiring)
  • Manually verified against a live Navidrome server (~1,200 favorites):
    two way star/unstar, offline queue incl. force stop + cold start delivery,
    first sync migration of preexisting favorites, Liked tab open/pull refresh,
    hearts surviving full syncs.

A small note

I'm not highly experienced with Kotlin, so I've used AI tools where needed to help write this
code. I've reviewed all of it to the best of my ability and tested it heavily
on my own server, but I'm very open to feedback, happy to explain, rework, or
split anything.

I have more Navidrome improvements planned (playlist write back, server
lyrics, offline downloads) which I'll submit as separate PRs to keep each one
reviewable. Thanks for reading!

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