fix(android): hydrate missing Home entities so sections actually render
Audit v3 §2 + §4.5: HomeRepository's hydrateAlbums / hydrateArtists / hydrateTracks use mapNotNull on the per-entity DAO, so any ID in /api/home/index that the local cache hasn't seen gets silently dropped from the emitted list. Combined with HomeSuccessContent hiding empty sections, the user sees the home view missing entire rows — Rediscover is the worst hit because by definition those are albums you HAVEN'T played recently, least likely to be in cache. User reported on emulator: 'we're missing a lot of rows and formatting from the home view, the rediscover section is completely missing and a number of the shown sections are rendered with a different number of rows than the flutter app has.' Fix: after refreshIndex() pulls the section ID lists, fire-and-forget a hydration pass on the app scope. For each section, find IDs the cache doesn't have, fetch via LibraryRepository's existing per-entity endpoints (refreshAlbumDetail / refreshArtistDetail / refreshTrack — the last already commented 'for the hydration queue'), chunked HYDRATE_CONCURRENCY=4 wide so we don't fire 50 parallel requests. runCatching per-call so one broken ID doesn't fail the batch. This is the slim version of audit #24 MetadataPrefetcher scoped to Home — the full background hydration queue with tile-providers is still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,17 +6,26 @@ import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedHomeIndexDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedHomeIndexEntity
|
||||
import com.fabledsword.minstrel.di.ApplicationScope
|
||||
import com.fabledsword.minstrel.library.data.LibraryRepository
|
||||
import com.fabledsword.minstrel.library.data.toDomain
|
||||
import com.fabledsword.minstrel.models.AlbumRef
|
||||
import com.fabledsword.minstrel.models.ArtistRef
|
||||
import com.fabledsword.minstrel.models.TrackRef
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val HYDRATE_CONCURRENCY = 4
|
||||
|
||||
/**
|
||||
* Cache-first reads for the Home screen. Per-section ID lists live in
|
||||
* `cached_home_index`; entity rows live in `cached_albums` /
|
||||
@@ -38,6 +47,8 @@ class HomeRepository @Inject constructor(
|
||||
private val albumDao: CachedAlbumDao,
|
||||
private val artistDao: CachedArtistDao,
|
||||
private val trackDao: CachedTrackDao,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
@ApplicationScope private val appScope: CoroutineScope,
|
||||
retrofit: Retrofit,
|
||||
) {
|
||||
private val api: HomeApi = retrofit.create()
|
||||
@@ -59,7 +70,17 @@ class HomeRepository @Inject constructor(
|
||||
|
||||
/**
|
||||
* Pulls /api/home/index and replaces each section in cached_home_index.
|
||||
* Caller (ViewModel) is responsible for catching network errors.
|
||||
* Also fires a fire-and-forget hydration pass for any entity IDs
|
||||
* the cache doesn't have yet — without this, Rediscover and
|
||||
* Most-Played tiles silently disappear because [hydrateAlbums] /
|
||||
* [hydrateTracks] mapNotNull-drop unknown rows.
|
||||
*
|
||||
* Hydration runs on the app scope so refreshIndex() returns as
|
||||
* soon as the index is in place; the Flow re-emits each section
|
||||
* as missing rows land.
|
||||
*
|
||||
* Caller (ViewModel) is responsible for catching network errors
|
||||
* from the index fetch itself.
|
||||
*/
|
||||
suspend fun refreshIndex() {
|
||||
val wire = api.getHomeIndex()
|
||||
@@ -68,6 +89,44 @@ class HomeRepository @Inject constructor(
|
||||
replaceSection(SECTION_REDISCOVER_ARTISTS, "artist", wire.rediscoverArtists)
|
||||
replaceSection(SECTION_MOST_PLAYED_TRACKS, "track", wire.mostPlayedTracks)
|
||||
replaceSection(SECTION_LAST_PLAYED_ARTISTS, "artist", wire.lastPlayedArtists)
|
||||
appScope.launch {
|
||||
hydrateMissingAlbums((wire.recentlyAddedAlbums + wire.rediscoverAlbums).distinct())
|
||||
hydrateMissingArtists((wire.rediscoverArtists + wire.lastPlayedArtists).distinct())
|
||||
hydrateMissingTracks(wire.mostPlayedTracks)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hydrateMissingAlbums(ids: List<String>) {
|
||||
val missing = ids.filter { albumDao.getById(it) == null }
|
||||
missing.chunked(HYDRATE_CONCURRENCY).forEach { batch ->
|
||||
coroutineScope {
|
||||
batch.map { id ->
|
||||
async { runCatching { libraryRepository.refreshAlbumDetail(id) } }
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hydrateMissingArtists(ids: List<String>) {
|
||||
val missing = ids.filter { artistDao.getById(it) == null }
|
||||
missing.chunked(HYDRATE_CONCURRENCY).forEach { batch ->
|
||||
coroutineScope {
|
||||
batch.map { id ->
|
||||
async { runCatching { libraryRepository.refreshArtistDetail(id) } }
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hydrateMissingTracks(ids: List<String>) {
|
||||
val missing = ids.filter { trackDao.getById(it) == null }
|
||||
missing.chunked(HYDRATE_CONCURRENCY).forEach { batch ->
|
||||
coroutineScope {
|
||||
batch.map { id ->
|
||||
async { runCatching { libraryRepository.refreshTrack(id) } }
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun replaceSection(section: String, entityType: String, ids: List<String>) {
|
||||
|
||||
Reference in New Issue
Block a user