Skip to content

Fix critical/high/medium code-review findings#4

Closed
lostf1sh wants to merge 4 commits into
mainfrom
fix/code-review-chm-findings
Closed

Fix critical/high/medium code-review findings#4
lostf1sh wants to merge 4 commits into
mainfrom
fix/code-review-chm-findings

Conversation

@lostf1sh

@lostf1sh lostf1sh commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two parts:

  1. Resolve 93 of 94 critical/high/medium findings from the multi-agent code review (every finding re-verified against current code first — all 94 reproduced, zero false positives).
  2. Squash the inherited Room migrations into a clean v1 schema baseline for the first F-Droid release.

Part 1 — Findings

Database & data integrity

  • album_art_themes gets a composite PK (albumArtUriString, paletteStyle) so each palette style is cached separately (now baked into the v1 baseline).
  • Clear playlist_songs on playlist delete (deletePlaylistWithSongs).
  • Guard deleteOrphanedArtists against artists still referenced by the NOT NULL songs.artist_id.
  • Dedupe the per-playlist default transition rule in the DAO (no schema change).

Concurrency & reliability

  • Remove main-thread runBlocking Coil artwork fetches from MediaItemBuilder (suspend conversion).
  • Make StateHolder scope release identity-safe so a sibling ViewModel can't null a live PlayerViewModel's scope.
  • Run long FULL/REBUILD library syncs as a dataSync foreground service.
  • Jellyfin library re-sync gated on a 24h staleness window.

Security & UX

  • Warn when Navidrome credentials fall back to unencrypted storage.
  • Add the missing package declaration to DelimiterConfigScreen.
  • Move hardcoded strings to resources; typed Jellyfin sync banner; one-shot transition save event; remember album color-scheme flows; remove a dead duplicate enum.

Part 2 — Migration squash (v1 baseline)

The only prior GitHub release (v0.7.0-foss, versionCode 8) is a throwaway with no real installed base; the first real distribution is the upcoming F-Droid build. So no device needs the inherited v3..v46 upgrade path.

  • @Database version 46 → 1; all 43 MIGRATION_x_y objects and migration-only helpers removed.
  • Fresh-install machinery kept: createRuntimeArtifactsCallback still creates songs_fts + the favorite/FTS sync triggers in onCreate.
  • AppModule: addMigrations(...) dropped; fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true) added so installing over the throwaway higher-versioned build wipes-and-recreates instead of crashing. Full destructive fallback stays debug-only.
  • Exported schema regenerated as 1.json; stale 43/44/45/46.json removed; obsolete PixelPlayerDatabaseMigrationTest removed.

All entity/DAO-level fixes ship as the v1 baseline. New upgrade migrations will be added normally from version 2 onward.

Verification

Check Result
:app:compileDebugKotlin
:app:testDebugUnitTest
:app:lintDebug
:app:compileDebugAndroidTestKotlin

Deferred — F99

Cloud song/album/artist IDs derive from 32-bit String.hashCode(). Re-deriving them changes the IDs that existing favorites (Room) and playlists (DataStore) reference; without a tested reconciliation migration it would risk silently orphaning that data. Left for a deliberately-designed migration (lower stakes now that v1 is a clean baseline, but still a data-shape change worth doing carefully).

Summary by CodeRabbit

  • New Features

    • Foreground sync notifications for long library rescans; library staleness detection
    • Structured Jellyfin sync banners with progress/success/error details
  • Bug Fixes

    • Playlist deletion now also removes associated songs
    • External-player overlay auto-closes on failed resolution
    • Safer, atomic file metadata updates to reduce corruption risk
  • Improvements

    • Navidrome warns when credentials are stored unencrypted
    • Localized file-explorer/offline/mini-player text; retry option
    • Sync retry/backoff and improved widget artwork caching

Resolve 93 of 94 verified critical/high/medium findings from the
multi-agent code review. Every finding was re-verified against the
current code before being fixed.

Database & data integrity:
- Add MIGRATION_45_46 (schema v46): album_art_themes gets a composite
  primary key (albumArtUriString, paletteStyle) so each palette style is
  cached as its own row instead of overwriting one row. Migration output
  verified to match the generated v46 schema.
- Clear playlist_songs when a playlist is deleted (deletePlaylistWithSongs)
  instead of orphaning rows.
- Guard deleteOrphanedArtists against artists still referenced by the
  NOT NULL songs.artist_id (its FK is SET_NULL), avoiding a transaction
  abort.
- Dedupe the per-playlist default transition rule in the DAO (the unique
  index treats NULL track ids as distinct), no schema change required.

Concurrency & reliability:
- Drop main-thread runBlocking Coil artwork fetches in MediaItemBuilder
  (suspend conversion across MediaItemBuilder / TrustedMediaItemsResolution
  / MusicService).
- Make StateHolder scope release identity-safe so a sibling ViewModel's
  onCleared cannot null a still-alive PlayerViewModel's scope.
- Run long FULL/REBUILD library syncs as a dataSync foreground service so
  they are not killed in the background.
- Jellyfin library re-sync is now gated on a 24h staleness window instead
  of running on every dashboard/home open.

Security & UX:
- Surface a warning when Navidrome credentials fall back to unencrypted
  storage.
- Add the missing package declaration to DelimiterConfigScreen.
- Move hardcoded user-facing strings to resources; model the Jellyfin sync
  banner as typed state; one-shot save event for transition settings;
  remember album color-scheme flows; remove a dead duplicate enum.

Verified: :app:compileDebugKotlin, :app:testDebugUnitTest and
:app:lintDebug all pass.

Deferred: F99 (cloud song/album/artist IDs derived from 32-bit
String.hashCode) is not auto-applied. Re-deriving IDs requires a
favorites/playlist reconciliation migration and on-device testing;
applying it blindly would risk silently orphaning cloud favorites and
playlists.
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebb76de5-9ef6-4079-be96-2a9d2be5fd54

📥 Commits

Reviewing files that changed from the base of the PR and between 2a95889 and 2236c18.

📒 Files selected for processing (2)
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt

📝 Walkthrough

Walkthrough

Adds foreground data-sync permission/service and worker retry/foreground promotion; adjusts Room entities, DAOs, and schema snapshots; hardens DataStore and backup import/export; refines repository cold-start and paging keys; localizes and stabilizes many Compose components and lifecycle cleanup; prewarms and palettes widget artwork with size-aware caching; makes media-file writes atomic; updates utilities, tests, and resources.

Changes

Comprehensive application updates

Layer / File(s) Summary
All changes (single checkpoint)
app/src/main/AndroidManifest.xml, app/src/main/java/.../PixelPlayerApplication.kt, app/src/main/java/.../data/worker/*, app/src/main/java/.../data/database/*, app/src/main/java/.../data/preferences/*, app/src/main/java/.../data/repository/*, app/src/main/java/.../presentation/*, app/src/main/java/.../ui/glancewidget/*, app/src/main/java/.../utils/*, app/src/main/res/values/*, app/src/test/*
Foreground sync wiring and notification channel; worker retry, backoff, and foreground promotion; Room entity/PK and DAO transactional fixes; schema snapshot updates; DataStore corruption handling and backup type/secret redaction; repository cold-start filter and absolute-offset paging; Compose localization and lifecycle ownership changes; Glance widget palette, prewarm, size-aware cache keys and SHA-256 keys; atomic media file replacement and decoder/resource cleanup; test and string resource additions.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A rabbit taps sync drums in time,
Foreground lights blink—workers climb.
Widgets glow with album hue,
Playlists export, neat and true.
Text un-jumbled, bytes made safe—
Hop! The library’s in good shape.
🥕✨

✨ 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 fix/code-review-chm-findings

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt (1)

121-155: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical concurrency race in cold-start directory filter resolution.

The @Volatile flag dirFilterResolved and cachedDirFilter.value read in resolvedDirFilter() have a race condition:

  1. onEach { dirFilterResolved = true } (line 140) performs a volatile write.
  2. stateIn updates cachedDirFilter.value immediately after (synchronized write).
  3. Another thread can observe dirFilterResolved == true via the volatile read but still see the stale default CachedDirFilter() from cachedDirFilter.value because there is no happens-before relationship between the volatile write in onEach and the StateFlow update in stateIn.

Impact: During the narrow window between steps 1 and 2 of the first emission, resolvedDirFilter() can return CachedDirFilter(applyFilter = false), bypassing directory filtering and leaking songs from blocked directories. This defeats the privacy/directory-filtering boundary that this code is intended to enforce.

🔒 Proposed fix using CompletableDeferred for proper synchronization

Replace the @Volatile flag with a CompletableDeferred to ensure the first real value is ready before returning the cached value:

-    /**
-     * Becomes true once [cachedDirFilter] has produced its first real value (i.e. the
-     * DataStore-backed directory preferences have emitted and the filter has been computed).
-     * Until then [cachedDirFilter] still holds the seeded default (applyFilter = false), which
-     * would incorrectly bypass the directory filter for users who have blocked directories.
-     */
-    `@Volatile` private var dirFilterResolved = false
+    /**
+     * Completed once [cachedDirFilter] has produced its first real value (i.e. the
+     * DataStore-backed directory preferences have emitted and the filter has been computed).
+     * Until then [cachedDirFilter] still holds the seeded default (applyFilter = false), which
+     * would incorrectly bypass the directory filter for users who have blocked directories.
+     */
+    private val dirFilterReady = CompletableDeferred<Unit>()
 
     private val cachedDirFilter: StateFlow<CachedDirFilter> = combine(
         userPreferencesRepository.allowedDirectoriesFlow,
         userPreferencesRepository.blockedDirectoriesFlow
     ) { allowed, blocked ->
         val (dirs, apply) = DirectoryFilterUtils.computeAllowedParentDirs(
             allowedDirs = allowed,
             blockedDirs = blocked,
             getAllParentDirs = { musicDao.getDistinctParentDirectories() },
             normalizePath = ::normalizePath
         )
         CachedDirFilter(dirs, apply)
-    }.onEach { dirFilterResolved = true }
+    }.onEach { dirFilterReady.complete(Unit) }
         .stateIn(repositoryScope, SharingStarted.Eagerly, CachedDirFilter())
 
     /**
      * Returns a resolved directory filter for one-shot reads. If [cachedDirFilter] has not yet
      * produced its first real value (cold-start window), computes a fresh filter from the
      * preference flows instead of returning the seeded default — otherwise blocked-directory
      * songs could leak into queues/library on the first access after process start.
      */
     private suspend fun resolvedDirFilter(): CachedDirFilter {
-        if (dirFilterResolved) return cachedDirFilter.value
-        val allowedDirs = userPreferencesRepository.allowedDirectoriesFlow.first()
-        val blockedDirs = userPreferencesRepository.blockedDirectoriesFlow.first()
-        val (allowedParentDirs, applyFilter) = computeAllowedDirs(allowedDirs, blockedDirs)
-        return CachedDirFilter(allowedParentDirs, applyFilter)
+        dirFilterReady.await()
+        return cachedDirFilter.value
     }

This ensures that resolvedDirFilter() will suspend until the first real emission completes, eliminating the race while preserving the caching benefit.

🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt`
around lines 121 - 155, The volatile flag dirFilterResolved is racy with
cachedDirFilter.value; replace it with a CompletableDeferred<CachedDirFilter>
(e.g. firstDirFilterReady) that you complete from the onEach block when the
first real CachedDirFilter is emitted, and in resolvedDirFilter() suspend on
firstDirFilterReady.await() (or await with timeout if needed) before returning
cachedDirFilter.value; update usages of dirFilterResolved and its writes/reads
to complete/await the CompletableDeferred so callers cannot observe the volatile
flag true while cachedDirFilter still holds the seeded default.
app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt (1)

1920-1953: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject overflowed numeric coercions during restore.

Long.toInt() can wrap, and the current Double/Long -> Int/Long coercions also silently truncate out-of-range values. A malformed backup can therefore restore negative or nonsensical settings instead of being skipped. Add range/integrality checks before writing narrowed numeric types.

Suggested fix
                 when (effectiveType) {
                     "int" -> {
-                        val value = entry.intValue
-                            ?: entry.doubleValue?.toInt()
-                            ?: entry.longValue?.toInt()
-                            ?: return@forEach
+                        val value = entry.intValue
+                            ?: entry.longValue
+                                ?.takeIf { it in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong() }
+                                ?.toInt()
+                            ?: entry.doubleValue
+                                ?.takeIf {
+                                    it.isFinite() &&
+                                        it % 1.0 == 0.0 &&
+                                        it in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()
+                                }
+                                ?.toInt()
+                            ?: return@forEach
                         preferences[intPreferencesKey(entry.key)] = value
                     }
                     "long" -> {
-                        val value = entry.longValue
-                            ?: entry.doubleValue?.toLong()
-                            ?: entry.intValue?.toLong()
-                            ?: return@forEach
+                        val value = entry.longValue
+                            ?: entry.intValue?.toLong()
+                            ?: entry.doubleValue
+                                ?.takeIf {
+                                    it.isFinite() &&
+                                        it % 1.0 == 0.0 &&
+                                        it in Long.MIN_VALUE.toDouble()..Long.MAX_VALUE.toDouble()
+                                }
+                                ?.toLong()
+                            ?: return@forEach
                         preferences[longPreferencesKey(entry.key)] = value
                     }
🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt`
around lines 1920 - 1953, When restoring numeric prefs in
UserPreferencesRepository (the block using
entry.intValue/entry.doubleValue/entry.longValue and writing to
intPreferencesKey/longPreferencesKey/androidx.datastore.preferences.core.floatPreferencesKey/androidx.datastore.preferences.core.doublePreferencesKey),
reject any narrowing coercions that would overflow or lose integrality: before
converting Double->Int/Long or Long->Int, check the source value is finite,
within the target type range (Int.MIN_VALUE..Int.MAX_VALUE or
Long.MIN_VALUE..Long.MAX_VALUE) and—for Double sources—has no fractional part
when targeting integer types; if a check fails, skip that entry (return@forEach)
instead of performing toInt()/toLong() truncation. Also apply similar
range/finite checks when narrowing Double->Float (ensure value is finite and
within Float.MIN_VALUE..Float.MAX_VALUE or use Float.isFinite) and when using
Long.toInt() ensure the long is within Int range before assignment.
🧹 Nitpick comments (1)
app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navigation/Screen.kt (1)

29-29: genreId route encoding is consistent with the current decoding, so the “+ becomes mismatch” concern isn’t supported.
Screen.kt encodes genreId with java.net.URLEncoder.encode(...), and the read path decodes it with java.net.URLDecoder.decode(...) (in both GenreDetailScreen and GenreDetailViewModel via savedStateHandle). This makes + round-trip back to spaces, so the specific "Hip Hop" -> "Hip+Hop" mismatch claim doesn’t match the implemented flow.
Optional cleanup: align path-segment semantics by switching to Uri.encode/Uri.decode and ensure decoding happens only once.

Suggested fix
-import java.net.URLEncoder
+import android.net.Uri
@@
-        fun createRoute(genreId: String) = "genre_detail/" + URLEncoder.encode(genreId, "UTF-8")
+        fun createRoute(genreId: String) = "genre_detail/${Uri.encode(genreId)}"
🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navigation/Screen.kt`
at line 29, The current createRoute function (createRoute in Screen.kt) uses
java.net.URLEncoder.encode while the decode happens via URLDecoder in
GenreDetailScreen and GenreDetailViewModel through savedStateHandle — this works
but is inconsistent with Android URI semantics; replace URLEncoder.encode(...)
with android.net.Uri.encode(...) in createRoute and migrate corresponding decode
calls to android.net.Uri.decode(...) (or remove duplicate decoding so decoding
happens exactly once in the savedStateHandle/GenreDetailScreen path) to keep
encoding/decoding consistent and avoid double-decoding issues.
🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt`:
- Around line 397-398: lastFullSyncTime is being set unconditionally even when
partial syncs fail; change JellyfinRepository so that lastFullSyncTime is only
updated after a fully successful run of syncUnifiedLibrarySongsFromJellyfin(),
syncLibrarySongs(), and any playlist syncs: detect failures (exceptions or error
results) from syncUnifiedLibrarySongsFromJellyfin() and syncLibrarySongs() and
only assign lastFullSyncTime = System.currentTimeMillis() when all these
operations complete without errors (or return success), otherwise leave the
timestamp unchanged and propagate/log the failure so the stale-gate will retry.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/media/SongMetadataEditor.kt`:
- Around line 584-606: The current fallback after staging.renameTo(target) uses
copyIntoPlaceDurably(target, staging), which reopens target with
FileOutputStream(target, false) and can truncate the original media file
mid-copy; instead, change the fallback in the safe-replace path (the block
handling renamed = staging.renameTo(target) in SongMetadataEditor.kt) to fail
the edit rather than attempt an in-place overwrite: log a clear warning/error
and return false when renameTo fails, and remove/avoid calling
copyIntoPlaceDurably in this branch (apply the same change to the other
identical fallback around the lines 609–616), so we never truncate the original
media file when rename cannot be used.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/paging/MediaStorePagingSource.kt`:
- Around line 36-37: The refresh key reconstruction is using
state.config.pageSize which can mismatch the actual loaded page size; update the
logic in getRefreshKey (the code that reads anchorPage?.prevKey/nextKey) to use
the actual loaded page size from the anchor page (e.g., anchorPage?.data?.size)
when computing the offset, falling back to state.config.initialLoadSize or
state.config.pageSize if the anchor page size is unavailable, so the computed
prev/next keys align with the real loaded page bounds.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt`:
- Around line 1828-1833: The redaction is asymmetric: code currently filters out
secret/URL-like keys during export (using isLikelySecretKey) but clearExisting
still deletes those live keys, and the import code accepts them from a backup;
to fix, change the clearExisting logic in UserPreferencesRepository (the method
that wipes existing prefs before import) to preserve any keys where
isLikelySecretKey(keyName) returns true instead of deleting them, and ensure the
import/restore path also skips any incoming entries with
isLikelySecretKey(keyName) (same check used in the export mapNotNull). Update
both the clearExisting implementation and the import loop that writes prefs so
secret keys are neither removed nor written from backups.

In `@app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncWorker.kt`:
- Around line 158-184: The delete-detection uses
fetchMediaStoreIds(directoryResolver) before the runtime minSongDurationMs is
refreshed, so short tracks configured below the default 10000ms can be
misclassified as deleted; fix by loading the preference-backed threshold into
minSongDurationMs before the deletedIds computation (or modify
fetchMediaStoreIds to accept an explicit threshold parameter and pass the
resolved value) so that deletedIds (computed from
musicDao.getAllMediaStoreSongIds(), mediaStoreIds and SyncMode.REBUILD handling)
uses the correct duration cutoff.
- Around line 781-788: The incremental-sync change check treats a missing
MediaStore album artist as a change signal; update the condition that compares
existing.albumArtist and raw.albumArtist to ignore the cursor value when the
cursor didn't supply one (i.e., when raw.albumArtist == null) the same way the
genre guard does. Locate the boolean expression using existing.albumArtist and
raw.albumArtist in SyncWorker.kt and change it to only consider the cursor value
as a signal when raw.albumArtist != null (for example, require raw.albumArtist
== null || existing.albumArtist == raw.albumArtist) so songs with an existing
tag-parsed albumArtist aren't spuriously reprocessed or overwritten.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardViewModel.kt`:
- Around line 54-55: The Failed data class currently surfaces raw
Throwable.message to users (data class Failed(val message: String?) :
JellyfinSyncBanner) which can leak backend/OkHttp/server details; change the
code paths that construct Failed to map throwable types/messages to a small set
of user-facing reasons (e.g., "Network error", "Authentication failed", "Server
unavailable", "Unknown error") and pass those sanitized strings into Failed,
while logging the original throwable (stack trace and message) to the app
logger; update all places that create Failed from exceptions (the sync routines
that call Failed(...), referenced in the file around the usages you saw) to
perform this mapping before constructing Failed and keep
JellyfinSyncStatus.Error unchanged.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlaylistViewModel.kt`:
- Around line 1113-1117: The resolver.update call that finalizes the MediaStore
item must be checked: after calling resolver.update(itemUri, finalize, null,
null) in PlaylistViewModel, validate the returned updateCount is 1 and if not,
throw an exception (e.g., IOException) so the existing catch/cleanup logic runs
and the pending row is removed; reference the local variables resolver,
finalize, itemUri and playlist.name when adding the error message to aid
debugging.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SongRemovalStateHolder.kt`:
- Line 70: The deletion currently force-casts Song.id with song.id.toLong() in
SongRemovalStateHolder, which can throw for non-numeric IDs; update the flow to
avoid hard-casting by (a) extending the repository contract (e.g., add
musicRepository.deleteByIdentifier(String id) or
musicRepository.deleteByCanonicalId(String id)) and call that with song.id for
non-numeric identifiers, or (b) if numeric deletion is required, first attempt a
safe parse (e.g., tryParse/NumberFormatException guard) and only call
musicRepository.deleteById(parsedLong) when parsing succeeds; update the
SongRemovalStateHolder call site to use the new repository method or the guarded
parse so no runtime exception is possible.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/PixelPlayerGlanceWidget.kt`:
- Around line 122-128: The loop currently prewarms album art for every item in
playerInfo.queue causing unnecessary work; restrict prewarming to only the
thumbnails the widget can render (four for the ExtraLarge layout). Update the
loop that calls decodeIntoCacheIfAbsent to iterate only the first four queue
items (or use ExtraLargeWidgetLayout.THUMBNAIL_COUNT if that constant exists)
while keeping the same queueTargetPx calculation and behavior.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetUtils.kt`:
- Around line 30-42: The getKey function currently only considers content hash,
causing different size variants of the same artwork to share a cache entry and
potentially serve smaller cached bitmaps to larger widgets. Modify the getKey
function to accept render dimensions (width and height parameters) in addition
to the byteArray, and include these dimensions in the returned cache key string
by appending them to the hex-encoded SHA-256 digest. Also identify any
URI-backed key generation functions (likely other overloaded versions or related
caching methods) that generate keys similarly and apply the same
dimension-inclusion logic to them.

In `@app/src/main/java/com/lostf1sh/pixelplayeross/utils/Extensions.kt`:
- Around line 47-55: MOJIBAKE_PUNCTUATION_REGEX currently only checks for "â"
followed by Windows-1252 special chars, missing ISO-8859-1 C1 control sequence
mojibake like �/�; update the Regex used by MOJIBAKE_PUNCTUATION_REGEX to
include the Unicode range U+0080..U+00BF (e.g. \u0080-\u00BF) in the character
class so that String.hasMojibakeSignature() will detect those ISO-8859-1
mojibake sequences as well.

---

Outside diff comments:
In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt`:
- Around line 1920-1953: When restoring numeric prefs in
UserPreferencesRepository (the block using
entry.intValue/entry.doubleValue/entry.longValue and writing to
intPreferencesKey/longPreferencesKey/androidx.datastore.preferences.core.floatPreferencesKey/androidx.datastore.preferences.core.doublePreferencesKey),
reject any narrowing coercions that would overflow or lose integrality: before
converting Double->Int/Long or Long->Int, check the source value is finite,
within the target type range (Int.MIN_VALUE..Int.MAX_VALUE or
Long.MIN_VALUE..Long.MAX_VALUE) and—for Double sources—has no fractional part
when targeting integer types; if a check fails, skip that entry (return@forEach)
instead of performing toInt()/toLong() truncation. Also apply similar
range/finite checks when narrowing Double->Float (ensure value is finite and
within Float.MIN_VALUE..Float.MAX_VALUE or use Float.isFinite) and when using
Long.toInt() ensure the long is within Int range before assignment.

In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt`:
- Around line 121-155: The volatile flag dirFilterResolved is racy with
cachedDirFilter.value; replace it with a CompletableDeferred<CachedDirFilter>
(e.g. firstDirFilterReady) that you complete from the onEach block when the
first real CachedDirFilter is emitted, and in resolvedDirFilter() suspend on
firstDirFilterReady.await() (or await with timeout if needed) before returning
cachedDirFilter.value; update usages of dirFilterResolved and its writes/reads
to complete/await the CompletableDeferred so callers cannot observe the volatile
flag true while cachedDirFilter still holds the seeded default.

---

Nitpick comments:
In
`@app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navigation/Screen.kt`:
- Line 29: The current createRoute function (createRoute in Screen.kt) uses
java.net.URLEncoder.encode while the decode happens via URLDecoder in
GenreDetailScreen and GenreDetailViewModel through savedStateHandle — this works
but is inconsistent with Android URI semantics; replace URLEncoder.encode(...)
with android.net.Uri.encode(...) in createRoute and migrate corresponding decode
calls to android.net.Uri.decode(...) (or remove duplicate decoding so decoding
happens exactly once in the savedStateHandle/GenreDetailScreen path) to keep
encoding/decoding consistent and avoid double-decoding issues.
🪄 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: 45890821-5937-4903-b52d-0ccf8ac875f9

📥 Commits

Reviewing files that changed from the base of the PR and between 804a79c and 6770598.

📒 Files selected for processing (85)
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/46.json
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/lostf1sh/pixelplayeross/PixelPlayerApplication.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/AlbumArtThemeEntity.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/LocalPlaylistDao.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/MusicDao.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/PixelPlayerDatabase.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/TransitionDao.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/media/SongMetadataEditor.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/navidrome/NavidromeRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/paging/MediaStorePagingSource.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/PlaylistPreferencesRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/TransitionRepositoryImpl.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/service/TrustedMediaItemsResolution.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/stats/PlaybackStatsRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/NavidromeSyncWorker.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncManager.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncWorker.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/EditSongSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/ExpressiveOfflineState.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/ExpressiveScrollBar.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/FileExplorerBottomSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/LyricsSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/PlaylistBottomSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/ReorderPresetsSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/ReorderTabsSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/SongInfoBottomSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/UnifiedPlayerSheetShared.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/external/ExternalPlayerOverlay.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/scoped/MiniPlayerDismissGestureHandler.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/scoped/SheetActionHandlers.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/library/LibraryTabId.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navidrome/dashboard/NavidromeDashboardScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navidrome/dashboard/NavidromeDashboardViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navigation/AppNavigation.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/navigation/Screen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/AlbumDetailScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/ArtistDetailScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/CreatePlaylistScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/DailyMixScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/DelimiterConfigScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/EditTransitionScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/GenreDetailScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/HomeScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/LibraryMediaTabs.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/LibraryScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/PlaylistDetailScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/RecentlyPlayedScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/SearchScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/SettingsScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/ConnectivityStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/DailyMixStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/EqualizerViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/GenreDetailViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/LibraryStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/ListeningStatsTracker.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/LyricsStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/MashupViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlaybackStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlayerViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlaylistViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SearchStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SettingsViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SleepTimerStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SongRemovalStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/ThemeStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/TransitionViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/PixelPlayerGlanceWidget.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetUtils.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/AudioDecoder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/AudioFileProvider.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/Extensions.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/LyricsUtils.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/MediaItemBuilder.kt
  • app/src/main/res/values/plurals.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/strings_presentation_batch_g.xml
  • app/src/test/java/com/lostf1sh/pixelplayeross/data/service/TrustedMediaItemsResolutionTest.kt
  • app/src/test/java/com/lostf1sh/pixelplayeross/presentation/library/LibraryTabIdTest.kt
💤 Files with no reviewable changes (4)
  • app/src/test/java/com/lostf1sh/pixelplayeross/presentation/library/LibraryTabIdTest.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/ReorderTabsSheet.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/library/LibraryTabId.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/components/LyricsSheet.kt

Comment thread app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt Outdated
Comment thread app/src/main/java/com/lostf1sh/pixelplayeross/data/media/SongMetadataEditor.kt Outdated
Comment on lines +158 to 184
// --- DELETION DETECTION PHASE ---
// Detect deleted songs efficiently using ID comparison. We do this for
// INCREMENTAL and FULL modes. REBUILD clears everything anyway.
//
// NOTE: detection only — the actual removal is deferred and folded into the
// same @Transaction as the upsert below (incrementalSyncMusicData accepts a
// deletedSongIds parameter). Deleting here in a separate transaction risked a
// failure between the committed delete and the upsert, leaving rows deleted but
// their replacements unwritten until the next successful sync.
val deletedIds: List<Long> =
if (syncMode != SyncMode.REBUILD) {
// Only compare MediaStore-backed songs; cloud sources are excluded.
val localSongIds = musicDao.getAllMediaStoreSongIds().toHashSet()
val mediaStoreIds = fetchMediaStoreIds(directoryResolver)

// Identify IDs that are in local DB but not in MediaStore
val ids = (localSongIds - mediaStoreIds).toList()
if (ids.isNotEmpty()) {
Timber.tag(TAG)
.i("Found ${ids.size} deleted songs. Removal deferred to upsert transaction...")
} else {
Timber.tag(TAG).d("No deleted songs found.")
}
ids
} else {
Timber.tag(TAG).d("No deleted songs found.")
emptyList()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Load the duration filter before computing deletedIds.

This block calls fetchMediaStoreIds(directoryResolver) before Line 148 refreshes minSongDurationMs from preferences, so the delete phase always uses the default 10000 ms cutoff. Any library configured below 10 seconds can have valid short tracks misclassified as deleted and removed on the next sync.

Move the min-duration read above the deletion phase, or pass the resolved threshold into fetchMediaStoreIds() instead of relying on the field state.

🤖 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 `@app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncWorker.kt`
around lines 158 - 184, The delete-detection uses
fetchMediaStoreIds(directoryResolver) before the runtime minSongDurationMs is
refreshed, so short tracks configured below the default 10000ms can be
misclassified as deleted; fix by loading the preference-backed threshold into
minSongDurationMs before the deletedIds computation (or modify
fetchMediaStoreIds to accept an explicit threshold parameter and pass the
resolved value) so that deletedIds (computed from
musicDao.getAllMediaStoreSongIds(), mediaStoreIds and SyncMode.REBUILD handling)
uses the correct duration cutoff.

Comment thread app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetUtils.kt Outdated
Comment thread app/src/main/java/com/lostf1sh/pixelplayeross/utils/Extensions.kt Outdated
The only prior GitHub release (v0.7.0-foss, versionCode 8) is a throwaway
with no real installed base; the first real distribution is the upcoming
F-Droid build. So there is no device whose database needs the inherited
v3..v46 upgrade path.

- Reset @database version 46 -> 1 and remove all 43 MIGRATION_x_y objects
  and their migration-only helpers (recreate*/getTableColumns/addColumnIfMissing/
  ensure*/rebuildSongsSearchIndex, etc.).
- Keep the fresh-install machinery: createRuntimeArtifactsCallback still
  installs the songs_fts virtual table and the favorite/FTS sync triggers in
  onCreate (Room does not manage these from the entity list).
- AppModule: drop addMigrations(...); add fallbackToDestructiveMigrationOnDowngrade
  so installing over the throwaway higher-versioned build wipes-and-recreates
  instead of crashing. Full destructive fallback stays debug-only.
- Regenerate the exported schema as 1.json; remove stale 43/44/45/46.json.
- Remove the now-obsolete PixelPlayerDatabaseMigrationTest.

All entity/DAO-level fixes from the previous commit are preserved and now
ship as the v1 baseline (e.g. album_art_themes composite primary key,
transition_rules unique index, deleteOrphanedArtists guard). New upgrade
migrations will be added normally from version 2 onward.

Verified: :app:compileDebugKotlin, :app:testDebugUnitTest, :app:lintDebug
and :app:compileDebugAndroidTestKotlin all pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt (1)

352-356: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact user identifiers from logged URLs too.

HEADERS logging still prints the full request URL, and the current redaction list misses Navidrome's u username param and Jellyfin's UserId. Those values will still end up in logcat for every debug request.

Suggested fix
-            redactQueryParams("t", "s", "api_key")
+            redactQueryParams("t", "s", "api_key", "u", "UserId")
🤖 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 `@app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt` around lines
352 - 356, The request logging redaction currently calls redactQueryParams("t",
"s", "api_key") in AppModule.kt but misses Navidrome's username param and
Jellyfin's user identifier; update the redactQueryParams call (the one invoked
where HTTP logging is configured) to include "u" and "UserId" so those query
parameters are also redacted from HEADERS-level logs.
app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/1.json (1)

640-665: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align songs.artist_id with its FK action before freezing v1.

This baseline still declares artist_id as NOT NULL while the FK uses ON DELETE SET NULL. Any direct delete from artists will fail instead of applying the declared action. Since this snapshot is now version 1, please fix the root contract now by either making artist_id nullable or changing the FK action to match the real invariant (RESTRICT/CASCADE).

Also applies to: 968-974

🤖 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
`@app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/1.json`
around lines 640 - 665, The schema declares artist_id as NOT NULL but the FK
uses ON DELETE SET NULL; update the contract so they match by making artist_id
nullable: change the createSql definition to remove the NOT NULL constraint for
`artist_id` (so the FOREIGN KEY `artist_id` can be set NULL on delete) and
update the corresponding fields entry (fieldPath "artistId", columnName
"artist_id") to set notNull: false; apply the same fix to the other occurrence
referenced (the second createSql/fields block) so both snapshots are consistent
with ON DELETE SET NULL.
🤖 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 `@app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt`:
- Around line 127-131: The Room builder currently calls
.fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true) unconditionally
which will wipe user data on downgrade; change this to only enable destructive
downgrade in non-release builds (e.g. guard the call behind BuildConfig.DEBUG or
an internal build flag) so release APKs never invoke
fallbackToDestructiveMigrationOnDowngrade. Locate the Room database builder in
AppModule.kt and wrap or conditionally add the
.fallbackToDestructiveMigrationOnDowngrade(...) invocation based on the
debug/internal build check (leave forward migrations unchanged).

---

Outside diff comments:
In
`@app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/1.json`:
- Around line 640-665: The schema declares artist_id as NOT NULL but the FK uses
ON DELETE SET NULL; update the contract so they match by making artist_id
nullable: change the createSql definition to remove the NOT NULL constraint for
`artist_id` (so the FOREIGN KEY `artist_id` can be set NULL on delete) and
update the corresponding fields entry (fieldPath "artistId", columnName
"artist_id") to set notNull: false; apply the same fix to the other occurrence
referenced (the second createSql/fields block) so both snapshots are consistent
with ON DELETE SET NULL.

In `@app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt`:
- Around line 352-356: The request logging redaction currently calls
redactQueryParams("t", "s", "api_key") in AppModule.kt but misses Navidrome's
username param and Jellyfin's user identifier; update the redactQueryParams call
(the one invoked where HTTP logging is configured) to include "u" and "UserId"
so those query parameters are also redacted from HEADERS-level logs.
🪄 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: 732a85e6-93e1-4300-8b78-718fc9267520

📥 Commits

Reviewing files that changed from the base of the PR and between 6770598 and 82b5a5b.

📒 Files selected for processing (6)
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/1.json
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/43.json
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/45.json
  • app/src/androidTest/java/com/lostf1sh/pixelplayeross/data/database/PixelPlayerDatabaseMigrationTest.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/database/PixelPlayerDatabase.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt
💤 Files with no reviewable changes (3)
  • app/src/androidTest/java/com/lostf1sh/pixelplayeross/data/database/PixelPlayerDatabaseMigrationTest.kt
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/43.json
  • app/schemas/com.lostf1sh.pixelplayeross.data.database.PixelPlayerDatabase/45.json

Comment on lines +127 to +131
// Schema starts at version 1 for the first public (F-Droid) release, so there are no
// upgrade migrations yet. If a throwaway pre-release build with a higher DB version is
// present, wipe-and-recreate on downgrade instead of crashing (no real data exists yet).
// Forward migrations are added normally from version 2 onward.
.fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't ship destructive downgrade fallback in release.

This is unconditional today, so any rollback from a future schema version to this APK will silently wipe playlists, favorites, and stats instead of refusing to open the newer DB. If this is only meant to bridge internal pre-release installs, gate it behind debug/internal builds rather than keeping it in the release builder path.

🤖 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 `@app/src/main/java/com/lostf1sh/pixelplayeross/di/AppModule.kt` around lines
127 - 131, The Room builder currently calls
.fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true) unconditionally
which will wipe user data on downgrade; change this to only enable destructive
downgrade in non-release builds (e.g. guard the call behind BuildConfig.DEBUG or
an internal build flag) so release APKs never invoke
fallbackToDestructiveMigrationOnDowngrade. Locate the Room database builder in
AppModule.kt and wrap or conditionally add the
.fallbackToDestructiveMigrationOnDowngrade(...) invocation based on the
debug/internal build check (leave forward migrations unchanged).

Skip 2 (one already fixed, one nitpick); fix 13: Jellyfin sync-stale gating and error sanitization, durable metadata replace, paging refresh key, backup secret-key redaction symmetry + numeric narrowing guards, albumArtist sync signal, playlist export finalize check, song-removal id parse, widget art prewarm + dimensioned cache key, mojibake regex C1 range, and a dir-filter resolution race.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt (1)

373-381: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate playlist-fetch failure instead of returning a success summary.

At Line 379, this branch returns Result.success(...) even when syncPlaylists() failed, so the ViewModel follows the success path and shows a synced banner for a failed run.

💡 Proposed minimal fix
-            val playlistResult = syncPlaylists().getOrElse {
+            val playlistResult = syncPlaylists().getOrElse { error ->
                 try {
                     syncUnifiedLibrarySongsFromJellyfin()
                 } catch (e: Exception) {
                     Timber.e(e, "$TAG: Failed to sync unified library after playlist fetch failure")
                 }
-                return@withContext Result.success(
-                    BulkSyncResult(playlistCount = 0, syncedSongCount = syncedSongCount, failedPlaylistCount = 0)
-                )
+                return@withContext Result.failure(error)
             }
🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt`
around lines 373 - 381, The current getOrElse branch swallows syncPlaylists()
failures and returns Result.success(...), causing the ViewModel to treat a
failed playlist fetch as success; change the error path so that when
syncPlaylists() fails you propagate the failure (return a Result.failure with
the original exception or rethrow) instead of returning Result.success. Update
the block that now calls syncUnifiedLibrarySongsFromJellyfin() and returns
BulkSyncResult to instead return Result.failure(it) (or wrap the caught
Exception) so callers see the failure; reference syncPlaylists(),
syncUnifiedLibrarySongsFromJellyfin(), and the BulkSyncResult-returning
withContext block to locate and modify the code.
♻️ Duplicate comments (1)
app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt (1)

76-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

URI-backed cache keys must include size dimensions.

The URI cache key still uses "uri:$rawUri" without incorporating the requested size, so different widget sizes will share the same cached bitmap. If an 80dp widget populates the cache first, a 120dp widget will hit the cache and reuse the smaller bitmap instead of decoding a sharper version at its target size. This is the same collision issue that was fixed for byte-array keys on lines 40-43, but it remains unresolved for URI-backed artwork.

🔧 Proposed fix to include size in URI cache keys
 } ?: albumArtUri?.let { rawUri ->
-    val cacheKey = "uri:$rawUri"
+    val cacheKey = "uri:$rawUri@${(size.value * context.resources.displayMetrics.density).toInt().coerceAtLeast(1)}"
     var bitmap = AlbumArtBitmapCache.getBitmap(cacheKey)
     if (bitmap == null) {
         bitmap = decodeAlbumArtFromUri(
             context = context,
             rawUri = rawUri,
             requestedSize = size
         )
         bitmap?.let { AlbumArtBitmapCache.putBitmap(cacheKey, it) }
     }
     bitmap?.let { ImageProvider(it) }
 }
🤖 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
`@app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt`
around lines 76 - 88, The URI-backed cache key is missing size dimensions so
different requested sizes collide; update the cacheKey construction where
albumArtUri is handled (the block using rawUri, cacheKey, AlbumArtBitmapCache,
decodeAlbumArtFromUri and ImageProvider) to incorporate the requested size (the
size variable passed into decodeAlbumArtFromUri) into the key so keys are unique
per URI+size (e.g., include width/height or size.value in the key) before
calling AlbumArtBitmapCache.getBitmap/putBitmap.
🤖 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.

Outside diff comments:
In
`@app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt`:
- Around line 373-381: The current getOrElse branch swallows syncPlaylists()
failures and returns Result.success(...), causing the ViewModel to treat a
failed playlist fetch as success; change the error path so that when
syncPlaylists() fails you propagate the failure (return a Result.failure with
the original exception or rethrow) instead of returning Result.success. Update
the block that now calls syncUnifiedLibrarySongsFromJellyfin() and returns
BulkSyncResult to instead return Result.failure(it) (or wrap the caught
Exception) so callers see the failure; reference syncPlaylists(),
syncUnifiedLibrarySongsFromJellyfin(), and the BulkSyncResult-returning
withContext block to locate and modify the code.

---

Duplicate comments:
In
`@app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt`:
- Around line 76-88: The URI-backed cache key is missing size dimensions so
different requested sizes collide; update the cacheKey construction where
albumArtUri is handled (the block using rawUri, cacheKey, AlbumArtBitmapCache,
decodeAlbumArtFromUri and ImageProvider) to incorporate the requested size (the
size variable passed into decodeAlbumArtFromUri) into the key so keys are unique
per URI+size (e.g., include width/height or size.value in the key) before
calling AlbumArtBitmapCache.getBitmap/putBitmap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9efd6b8-c798-467c-a69d-ac67c5ea1a06

📥 Commits

Reviewing files that changed from the base of the PR and between 82b5a5b and 2a95889.

📒 Files selected for processing (15)
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/JellyfinRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/media/SongMetadataEditor.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/paging/MediaStorePagingSource.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncWorker.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlaylistViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/SongRemovalStateHolder.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/PixelPlayerGlanceWidget.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetComponents.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/WidgetUtils.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/Extensions.kt
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/src/main/java/com/lostf1sh/pixelplayeross/utils/Extensions.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/jellyfin/dashboard/JellyfinDashboardScreen.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/presentation/viewmodel/PlaylistViewModel.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/worker/SyncWorker.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/repository/MusicRepositoryImpl.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt
  • app/src/main/java/com/lostf1sh/pixelplayeross/ui/glancewidget/PixelPlayerGlanceWidget.kt

Two follow-up code-review fixes:

- JellyfinRepository.syncAllPlaylistsAndSongs: return Result.failure on
  syncPlaylists() failure instead of masking it as Result.success, so the
  dashboard shows an error (and the stale-gate retries) rather than a green
  banner.
- WidgetComponents.AlbumArtImage: include the requested size in the URI cache
  key so the same artwork at different widget sizes (Bar 44dp / Control 80dp /
  Grid variable) no longer collides. These widgets don't prewarm, so no
  shared-key coordination is affected.
@lostf1sh lostf1sh closed this Jun 11, 2026
@lostf1sh lostf1sh deleted the fix/code-review-chm-findings branch June 17, 2026 13:47
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