From 4b38623229f555d6284c76e8d5dd000ca5ff9096 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 22 May 2026 14:10:05 -0400 Subject: [PATCH] feat(android): port cached_playlists + cached_playlist_tracks (M8 4.2 slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related entities mirroring flutter_client/lib/cache/db.dart: - CachedPlaylists — one row per playlist; `systemVariant` is null for user playlists and "for_you" / "songs_like_artist" / etc. for system-generated mixes (used by the add-to-playlist sheet filter) - CachedPlaylistTracks — composite-PK join table, `position` carries ordering inside a playlist DAO surfaces split user vs system playlists at the query layer so ViewModels don't have to filter — observeUserPlaylists/observeSystemPlaylists. PlaylistTrackDao gets a deleteByPlaylist for the replace-all pattern after a sync delta lands. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/cache/db/AppDatabase.kt | 8 ++++ .../cache/db/dao/CachedPlaylistDao.kt | 38 +++++++++++++++++++ .../cache/db/dao/CachedPlaylistTrackDao.kt | 36 ++++++++++++++++++ .../cache/db/entities/CachedPlaylistEntity.kt | 29 ++++++++++++++ .../db/entities/CachedPlaylistTrackEntity.kt | 19 ++++++++++ 5 files changed, 130 insertions(+) create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistTrackDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistEntity.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistTrackEntity.kt diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt index bc169ae5..d56a4442 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt @@ -6,11 +6,15 @@ import androidx.room.TypeConverters import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao +import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao +import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity +import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity +import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity @@ -34,6 +38,8 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity CachedAlbumEntity::class, CachedTrackEntity::class, CachedLikeEntity::class, + CachedPlaylistEntity::class, + CachedPlaylistTrackEntity::class, ], version = 1, exportSchema = true, @@ -45,4 +51,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun cachedAlbumDao(): CachedAlbumDao abstract fun cachedTrackDao(): CachedTrackDao abstract fun cachedLikeDao(): CachedLikeDao + abstract fun cachedPlaylistDao(): CachedPlaylistDao + abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistDao.kt new file mode 100644 index 00000000..1f0c2adc --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistDao.kt @@ -0,0 +1,38 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedPlaylistDao { + /** All playlists, user + system, sorted by name. */ + @Query("SELECT * FROM cached_playlists ORDER BY name COLLATE NOCASE ASC") + fun observeAll(): Flow> + + /** User playlists only (systemVariant IS NULL) — for the add-to-playlist sheet. */ + @Query( + "SELECT * FROM cached_playlists " + + "WHERE systemVariant IS NULL ORDER BY name COLLATE NOCASE ASC", + ) + fun observeUserPlaylists(): Flow> + + /** System playlists for the home screen. */ + @Query( + "SELECT * FROM cached_playlists " + + "WHERE systemVariant IS NOT NULL ORDER BY name COLLATE NOCASE ASC", + ) + fun observeSystemPlaylists(): Flow> + + @Query("SELECT * FROM cached_playlists WHERE id = :id") + suspend fun getById(id: String): CachedPlaylistEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(rows: List) + + @Query("DELETE FROM cached_playlists WHERE id IN (:ids)") + suspend fun deleteByIds(ids: List) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistTrackDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistTrackDao.kt new file mode 100644 index 00000000..f9fa32a6 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedPlaylistTrackDao.kt @@ -0,0 +1,36 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedPlaylistTrackDao { + @Query( + "SELECT * FROM cached_playlist_tracks " + + "WHERE playlistId = :playlistId ORDER BY position ASC", + ) + fun observeByPlaylist(playlistId: String): Flow> + + @Query( + "SELECT * FROM cached_playlist_tracks " + + "WHERE playlistId = :playlistId ORDER BY position ASC", + ) + suspend fun getByPlaylist(playlistId: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(rows: List) + + /** Replace-all pattern for a playlist; called after a sync delta lands. */ + @Query("DELETE FROM cached_playlist_tracks WHERE playlistId = :playlistId") + suspend fun deleteByPlaylist(playlistId: String) + + @Query( + "DELETE FROM cached_playlist_tracks " + + "WHERE playlistId = :playlistId AND trackId IN (:trackIds)", + ) + suspend fun deleteTracksFromPlaylist(playlistId: String, trackIds: List) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistEntity.kt new file mode 100644 index 00000000..8b0bb20a --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistEntity.kt @@ -0,0 +1,29 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * Cache row for one playlist (user or system). Mirrors + * `flutter_client/lib/cache/db.dart`'s `CachedPlaylists` Drift table. + * + * `systemVariant` is null for user playlists and one of + * "for_you" / "songs_like_artist" / "discover" / "todays_mix" / etc. + * for system-generated mixes. Lets the add-to-playlist sheet filter + * system playlists locally without a REST round-trip. + */ +@Entity(tableName = "cached_playlists") +data class CachedPlaylistEntity( + @PrimaryKey val id: String, + val userId: String, + val name: String, + val description: String = "", + val isPublic: Boolean = false, + val coverPath: String? = null, + val trackCount: Int = 0, + val durationSec: Int = 0, + val systemVariant: String? = null, + val fetchedAt: Instant = Clock.System.now(), +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistTrackEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistTrackEntity.kt new file mode 100644 index 00000000..ef29db40 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedPlaylistTrackEntity.kt @@ -0,0 +1,19 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity + +/** + * Ordered membership of tracks within a playlist. Mirrors + * `flutter_client/lib/cache/db.dart`'s `CachedPlaylistTracks` Drift table. + * Composite PK so the same track can only appear once per playlist; + * `position` carries the ordering. + */ +@Entity( + tableName = "cached_playlist_tracks", + primaryKeys = ["playlistId", "trackId"], +) +data class CachedPlaylistTrackEntity( + val playlistId: String, + val trackId: String, + val position: Int = 0, +)