diff --git a/android/app/src/main/java/com/fabledsword/minstrel/home/data/HomeRepository.kt b/android/app/src/main/java/com/fabledsword/minstrel/home/data/HomeRepository.kt index 5edb5473..c18429e2 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/home/data/HomeRepository.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/home/data/HomeRepository.kt @@ -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) { + 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) { + 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) { + 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) {