Skip to content

Add app-private offline caching for cloud music - #68

Open
lostf1sh wants to merge 3 commits into
mainfrom
codex/offline-media-cache
Open

Add app-private offline caching for cloud music#68
lostf1sh wants to merge 3 commits into
mainfrom
codex/offline-media-cache

Conversation

@lostf1sh

Copy link
Copy Markdown
Collaborator

Summary

  • add a small LRU cache for normal cloud streaming and a separate non-evicting app-private cache for user-selected offline media
  • let users cache songs, albums, and playlists and manage them from a new Cached library tab
  • add configurable offline cache limits, storage safeguards, background progress, Room metadata/refcounts, and resume support

Why

Issue #47 asks for server music to be available offline. This keeps offline audio inside the PixelPlayer Media3 cache instead of exporting user-visible files, while preserving a small transient cache for ordinary streaming.

Testing

  • JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :app:compileDebugKotlin
  • JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :app:testDebugUnitTest
  • JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :app:lintDebug
  • JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :app:assembleDebug -Ppixelplayer.enableAbiSplits=false
  • CodeRabbit review: 0 findings after fixes

Screenshots

Not included; no device or emulator runtime pass was performed for this change.

Closes #47

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds app-private offline caching for cloud music (Navidrome and Jellyfin) using a dual-cache design: a 256 MB LRU transient cache for normal streaming and a non-evicting filesDir-backed cache for user-selected offline media. Users can cache songs, albums, and playlists and manage them from a new "Cached" library tab, with configurable storage limits and background download progress.

  • Dual-cache routing (PlaybackAudioCache, CacheRoutingDataSource): cloud URIs carrying a pixelplayer:audio:v1: cache key are routed to the offline-first CacheDataSource chain; all other media hits the direct upstream, cleanly separating offline and transient traffic.
  • Room schema v4 (OfflineMediaEntities, MIGRATION_3_4): adds cached_collections, cached_tracks, and cached_collection_tracks cross-ref tables with FK cascades; ref-counting for shared tracks is handled by correlated subqueries in tracksExclusiveToCollection / deleteTracksExclusiveToCollection, avoiding the SQLite variable-count limit.
  • Download lifecycle (OfflineMediaRepository, OfflineMediaDownloadService): an appScope polling loop (750 ms active / 5 s idle) drives quota and low-storage enforcement via DownloadManager.setStopReason; resume-on-foregrounding is wired through PixelPlayerApplication.onStart.

Confidence Score: 5/5

Safe to merge; the two findings are non-blocking quality concerns with no user-visible data loss or correctness risk.

The offline cache architecture is solid: dual SimpleCache instances sharing a single StandaloneDatabaseProvider, correct CacheRoutingDataSource key-prefix routing, FK-cascade-safe deletion order in the DAO, correlated-subquery orphan detection that avoids the SQLite variable-count limit, and a well-guarded metadataReady initialization gate. The two findings are minor: StatFs called on the main thread (a StrictMode concern in debug builds, not a crash), and a fire-and-forget enforceRuntimeLimits that lets startForeground race ahead of stop-reason enforcement by one coroutine turn. Neither affects data integrity or offline playback correctness.

Files Needing Attention: OfflineMediaRepository.kt — specifically the enforceRuntimeLimits / resumePendingDownloads interaction and the StatFs call site.

Important Files Changed

Filename Overview
app/src/main/java/com/lostf1sh/pixelplayeross/data/offline/OfflineMediaRepository.kt Core offline cache orchestrator: download lifecycle, quota enforcement, and metadata persistence. Minor ordering issue in resumePendingDownloads (enforceRuntimeLimits is fire-and-forget before startForeground). StatFs called on main thread in enforceRuntimeLimits.
app/src/main/java/com/lostf1sh/pixelplayeross/data/service/player/PlaybackAudioCache.kt Well-structured dual-cache abstraction; CacheRoutingDataSource correctly routes by cache-key prefix; isAvailableOffline uses content-length metadata for a reliable offline check.
app/src/main/java/com/lostf1sh/pixelplayeross/data/database/OfflineMediaEntities.kt Room entities, DAO, and FK constraints look correct; tracksExclusiveToCollection uses correlated subqueries instead of IN lists, avoiding the SQLite variable-count limit.
app/src/main/java/com/lostf1sh/pixelplayeross/data/database/Migrations.kt MIGRATION_3_4 DDL matches Room entity annotations; unique nullable index on cache_key is correct for SQLite NULL semantics; FK cascade order is consistent with DAO deletion order.
app/src/main/java/com/lostf1sh/pixelplayeross/data/service/player/DualPlayerEngine.kt Cache-key injection in resolveDataSpec and offline-first routing in resolveMediaItem are correct; shouldUsePlaybackCache guard prevents accidental key overwrite for already-keyed items.
app/src/main/java/com/lostf1sh/pixelplayeross/data/offline/OfflineMediaDownloadService.kt Standard Media3 DownloadService subclass; correctly returns the singleton DownloadManager from the repository; RESTART intent-filter ensures the service can be restarted by the system.
app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/LibraryCachedTab.kt Composable cache management tab; uses AlertDialog for removal confirmation; progress indicator only shown for non-COMPLETE items; filter state is local and reset-safe.
app/src/main/java/com/lostf1sh/pixelplayeross/data/preferences/UserPreferencesRepository.kt Adds OFFLINE_CACHE_LIMIT_BYTES preference key, clamped between 1 GB and 100 GB (or 0 for unlimited); migration correctly appends CACHED tab to existing tab order lists.
app/src/main/java/com/lostf1sh/pixelplayeross/presentation/screens/SettingsCategoryScreen.kt Adds offline cache limit slider backed by OFFLINE_CACHE_LIMIT_OPTIONS; index-based mapping is safe given the fixed-length array and coerceIn guard.

Sequence Diagram

sequenceDiagram
    participant Player as ExoPlayer
    participant RDS as ResolvingDataSource
    participant CRD as CacheRoutingDataSource
    participant OC as OfflineCache (filesDir)
    participant TC as TransientCache (cacheDir)
    participant Net as Network

    Player->>RDS: "open(dataSpec, navidrome://... key=pixelplayer:audio:v1:)"
    RDS->>RDS: shouldUsePlaybackCache(key) true, resolve URI
    RDS->>CRD: open(dataSpec with cache key)
    CRD->>CRD: shouldUsePlaybackCache true, select cacheDataSource
    CRD->>OC: open / read
    alt Fully cached offline
        OC-->>CRD: bytes
        CRD-->>Player: bytes
    else Not in offline cache
        OC->>TC: open upstream (cache miss fallback)
        alt In transient cache
            TC-->>OC: bytes
            OC-->>CRD: bytes
        else Cache miss
            TC->>Net: fetch
            Net-->>TC: bytes stored in transient cache
            TC-->>OC: bytes
            OC-->>CRD: bytes
        end
        CRD-->>Player: bytes
    end
    Note over Player,Net: Non-cloud URIs use directDataSource, bypassing both caches
Loading

Reviews (3): Last reviewed commit: "Fix large offline collection removal" | Re-trigger Greptile

@itztrmin

Copy link
Copy Markdown

LGTM

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.

[Feature]: download songs/albums for offline use

2 participants