release v2026.05.13.1: player + Discover hotfix #44
+32
@@ -4,6 +4,7 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -35,4 +36,35 @@ interface CachedPlaylistDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM cached_playlists WHERE id IN (:ids)")
|
@Query("DELETE FROM cached_playlists WHERE id IN (:ids)")
|
||||||
suspend fun deleteByIds(ids: List<String>)
|
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.AppendTracksRequest
|
||||||
import com.fabledsword.minstrel.api.endpoints.PlaylistsApi
|
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.CachedPlaylistDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
|
||||||
@@ -39,6 +42,7 @@ class PlaylistsRepository @Inject constructor(
|
|||||||
private val playlistDao: CachedPlaylistDao,
|
private val playlistDao: CachedPlaylistDao,
|
||||||
private val playlistTrackDao: CachedPlaylistTrackDao,
|
private val playlistTrackDao: CachedPlaylistTrackDao,
|
||||||
private val mutationQueue: MutationQueue,
|
private val mutationQueue: MutationQueue,
|
||||||
|
private val authController: AuthController,
|
||||||
retrofit: Retrofit,
|
retrofit: Retrofit,
|
||||||
) {
|
) {
|
||||||
private val api: PlaylistsApi = retrofit.create()
|
private val api: PlaylistsApi = retrofit.create()
|
||||||
@@ -67,8 +71,21 @@ class PlaylistsRepository @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
suspend fun refreshList() {
|
suspend fun refreshList() {
|
||||||
val wire = api.list(kind = "all")
|
val wire = api.list(kind = "all")
|
||||||
val all = wire.owned + wire.public
|
val all = (wire.owned + wire.public).map { it.toEntity() }
|
||||||
playlistDao.upsertAll(all.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.
|
* track-membership IDs, not the display fields.
|
||||||
*/
|
*/
|
||||||
suspend fun refreshDetail(id: String): PlaylistDetailRef {
|
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()))
|
playlistDao.upsertAll(listOf(wire.toPlaylistEntity()))
|
||||||
playlistTrackDao.deleteByPlaylist(id)
|
playlistTrackDao.deleteByPlaylist(id)
|
||||||
playlistTrackDao.upsertAll(
|
playlistTrackDao.upsertAll(
|
||||||
|
|||||||
Reference in New Issue
Block a user