fix(android): reconcile playlist cache to prune stale system-playlist UUIDs
Tapping certain playlists showed 'That playlist no longer exists' because the server's BuildSystemPlaylists rotates system-playlist UUIDs every rebuild, and the LIST refresh only upserted -- stale UUIDs lingered in the cache, tapping them triggered a 404. Mirrors playlists_provider.dart: PlaylistsRepository.refreshList now deletes the user's cached rows not in the fresh owned set (catches both deleted user playlists AND old system UUIDs since system playlists are user-scoped) before upserting the fresh all, atomically via a new replaceList @Transaction on the DAO. Also deletes the cached row when refreshDetail 404s so a stale tap self-heals the list. Inject AuthController for the current user id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
@@ -4,6 +4,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -35,4 +36,35 @@ interface CachedPlaylistDao {
|
||||
|
||||
@Query("DELETE FROM cached_playlists WHERE id IN (:ids)")
|
||||
suspend fun deleteByIds(ids: List<String>)
|
||||
|
||||
@Query("DELETE FROM cached_playlists WHERE userId = :userId")
|
||||
suspend fun deleteAllOwned(userId: String)
|
||||
|
||||
@Query("DELETE FROM cached_playlists WHERE userId = :userId AND id NOT IN (:keepIds)")
|
||||
suspend fun deleteOwnedNotIn(userId: String, keepIds: List<String>)
|
||||
|
||||
/**
|
||||
* Atomically reconciles the cache against the fresh list response.
|
||||
* Mirrors `flutter_client/lib/playlists/playlists_provider.dart:54` —
|
||||
* `BuildSystemPlaylists` rotates system-playlist UUIDs every
|
||||
* rebuild, so upsert alone leaves stale rows whose detail fetch
|
||||
* 404s. Delete any of the user's rows not in [freshOwnedIds] (this
|
||||
* catches both deleted owned playlists AND old system-playlist
|
||||
* UUIDs, since system playlists are user-scoped and appear in
|
||||
* [all] under their new id rather than in [freshOwnedIds]), then
|
||||
* upsert the fresh set.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replaceList(
|
||||
userId: String,
|
||||
freshOwnedIds: List<String>,
|
||||
all: List<CachedPlaylistEntity>,
|
||||
) {
|
||||
if (freshOwnedIds.isEmpty()) {
|
||||
deleteAllOwned(userId)
|
||||
} else {
|
||||
deleteOwnedNotIn(userId, freshOwnedIds)
|
||||
}
|
||||
upsertAll(all)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-3
@@ -2,6 +2,9 @@ package com.fabledsword.minstrel.playlists.data
|
||||
|
||||
import com.fabledsword.minstrel.api.endpoints.AppendTracksRequest
|
||||
import com.fabledsword.minstrel.api.endpoints.PlaylistsApi
|
||||
import com.fabledsword.minstrel.auth.AuthController
|
||||
import retrofit2.HttpException
|
||||
import java.net.HttpURLConnection
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
||||
@@ -39,6 +42,7 @@ class PlaylistsRepository @Inject constructor(
|
||||
private val playlistDao: CachedPlaylistDao,
|
||||
private val playlistTrackDao: CachedPlaylistTrackDao,
|
||||
private val mutationQueue: MutationQueue,
|
||||
private val authController: AuthController,
|
||||
retrofit: Retrofit,
|
||||
) {
|
||||
private val api: PlaylistsApi = retrofit.create()
|
||||
@@ -67,8 +71,21 @@ class PlaylistsRepository @Inject constructor(
|
||||
*/
|
||||
suspend fun refreshList() {
|
||||
val wire = api.list(kind = "all")
|
||||
val all = wire.owned + wire.public
|
||||
playlistDao.upsertAll(all.map { it.toEntity() })
|
||||
val all = (wire.owned + wire.public).map { it.toEntity() }
|
||||
val userId = authController.currentUser.value?.id
|
||||
if (userId != null) {
|
||||
// Reconcile: BuildSystemPlaylists rotates system-playlist
|
||||
// UUIDs every rebuild, so upsert alone leaves stale rows
|
||||
// whose detail fetch 404s ("That playlist no longer
|
||||
// exists"). Mirrors playlists_provider.dart's deleteWhere.
|
||||
playlistDao.replaceList(
|
||||
userId = userId,
|
||||
freshOwnedIds = wire.owned.map { it.id },
|
||||
all = all,
|
||||
)
|
||||
} else {
|
||||
playlistDao.upsertAll(all)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +96,17 @@ class PlaylistsRepository @Inject constructor(
|
||||
* track-membership IDs, not the display fields.
|
||||
*/
|
||||
suspend fun refreshDetail(id: String): PlaylistDetailRef {
|
||||
val wire = api.get(id)
|
||||
val wire = try {
|
||||
api.get(id)
|
||||
} catch (e: HttpException) {
|
||||
// Server says this playlist is gone — drop the stale cache
|
||||
// row so the list stops showing it. Mirrors
|
||||
// playlists_provider.dart's deleteWhere on detail failure.
|
||||
if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
playlistDao.deleteByIds(listOf(id))
|
||||
}
|
||||
throw e
|
||||
}
|
||||
playlistDao.upsertAll(listOf(wire.toPlaylistEntity()))
|
||||
playlistTrackDao.deleteByPlaylist(id)
|
||||
playlistTrackDao.upsertAll(
|
||||
|
||||
Reference in New Issue
Block a user