diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index f818f7ff..bd17ad0a 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -240,6 +240,13 @@ internal const val ARTIST_ART_UPDATE_SQL = "FROM desired WHERE artists.rowid = desired.rowid AND " + "(artists.coverArts != desired.coverArts OR artists.downloadedCoverArts != desired.downloadedCoverArts)" +internal const val ARTIST_COVER_ARTS_SQL = + "SELECT DISTINCT albums.coverArt FROM ($ARTIST_COVER_CREDITS) credits " + + "JOIN albums ON albums.sourceId = :sourceId AND albums.id = credits.albumId " + + "WHERE credits.artistId = :artistId AND albums.coverArt IS NOT NULL " + + "ORDER BY CASE credits.creditType WHEN 'album' THEN 0 ELSE 1 END, albums.year DESC, " + + "albums.sortName COLLATE NOCASE, albums.id LIMIT 4" + internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL = "WITH downloaded_albums AS (SELECT DISTINCT artistId, albumId FROM ($DOWNLOADED_COVER_ALBUM_ROWS) " + "WHERE artistId IN (:artistIds)), " + @@ -425,6 +432,18 @@ interface LibraryDao { @Query(ARTIST_ART_UPDATE_SQL) suspend fun recomputeArtistArt(sourceId: Long) + @Query(ARTIST_COVER_ARTS_SQL) + suspend fun artistCoverArts( + sourceId: Long, + artistId: String, + ): List + + @Query(ARTIST_COVER_ARTS_SQL) + fun artistCoverArtsFlow( + sourceId: Long, + artistId: String, + ): Flow> + @Query(DOWNLOADED_ARTIST_ART_UPDATE_SQL) suspend fun recomputeDownloadedArtistArt( sourceId: Long, diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index b8a8f866..f752c60f 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -442,6 +442,11 @@ class LibraryRepository( } } + fun artistCoverArts( + sourceId: Long, + artistId: String, + ): Flow> = db.libraryDao().artistCoverArtsFlow(sourceId, artistId) + fun albumDiscs( sourceId: Long, albumId: String, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt index 7dd6ef24..5356d4ea 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt @@ -146,14 +146,57 @@ class ArtistDetailViewModel( init { viewModelScope.launch { combine( - artist.filterNotNull(), + artist, sourceRepository.offline, sourceRepository.activeSource, - ) { loaded, _, activeSource -> loaded to activeSource } - .collectLatest { (loaded, activeSource) -> - if (activeSource == null) return@collectLatest - val hero = sourceRepository.artistArt(loaded, forIcon = false) - val icon = sourceRepository.artistArt(loaded, forIcon = true) + sourceRepository.activeSource.filterNotNull().flatMapLatest { + libraryRepository.artistCoverArts(it, artistId) + }, + ) { loaded, offline, activeSource, covers -> ArtistArtInput(loaded, offline, activeSource, covers) } + .collectLatest { input -> + if (input.sourceId == null) return@collectLatest + val resolved = + if (input.covers.isEmpty()) { + input.artist + } else { + when { + input.artist == null -> { + Artist( + sourceId = input.sourceId, + id = artistId, + name = "", + albumCount = 0, + starred = null, + coverArts = input.covers, + downloadedCoverArts = if (input.offline) input.covers else emptyList(), + ) + } + + input.offline -> { + if (input.artist.downloadedCoverArts.isNullOrEmpty()) { + input.artist.copy(downloadedCoverArts = input.covers) + } else { + input.artist + } + } + + input.artist.coverArts == input.covers -> { + input.artist + } + + else -> { + input.artist.copy(coverArts = input.covers) + } + } + } + if (resolved == null) { + _art.value = null + _artThumbnail.value = null + _heroArt.value = null + return@collectLatest + } + val hero = sourceRepository.artistArt(resolved, forIcon = false) + val icon = sourceRepository.artistArt(resolved, forIcon = true) _art.value = hero.image ?: hero.covers.firstOrNull() _artThumbnail.value = icon.image ?: icon.covers.firstOrNull() _heroArt.value = hero @@ -172,3 +215,10 @@ class ArtistDetailViewModel( } } } + +private data class ArtistArtInput( + val artist: Artist?, + val offline: Boolean, + val sourceId: Long?, + val covers: List, +) diff --git a/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt b/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt index 0776694b..85321bdc 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/LibraryDaoTest.kt @@ -655,6 +655,52 @@ class LibraryDaoTest { ) } + @Test + fun coverArtFallbackFindsTrackCreditsWithoutAnArtistRow() = + runTest { + val sourceId = source() + val dao = db.libraryDao() + dao.upsertAlbums( + listOf( + album(sourceId, "al-1", "Album", year = 2020, starred = null) + .copy(artistId = "ar-other", coverArt = "track-art"), + album(sourceId, "al-2", "Featured", year = 2021, starred = null) + .copy(artistId = "ar-other", coverArt = "featured-art"), + ), + ) + dao.upsertSongs( + listOf( + song(sourceId, "s-1", "Song", starred = null) + .copy(albumId = "al-1", artistId = "ar-missing"), + song(sourceId, "s-2", "Featured", starred = null) + .copy(albumId = "al-2", artistId = "ar-other"), + ), + ) + db.useWriterConnection { connection -> + connection.usePrepared( + "INSERT INTO album_artists (sourceId, albumId, position, artistId, name) " + + "VALUES (?, 'al-2', 0, 'ar-featured', 'Featured')", + ) { statement -> + statement.bindLong(1, sourceId) + statement.step() + } + connection.usePrepared( + "INSERT INTO song_artists (sourceId, songId, position, artistId, name) " + + "VALUES (?, 's-2', 0, 'ar-guest', 'Guest')", + ) { statement -> + statement.bindLong(1, sourceId) + statement.step() + } + } + + assertNull(dao.artistOnce(sourceId, "ar-missing")) + assertNull(dao.artistOnce(sourceId, "ar-featured")) + assertNull(dao.artistOnce(sourceId, "ar-guest")) + assertEquals(listOf("track-art"), dao.artistCoverArts(sourceId, "ar-missing")) + assertEquals(listOf("featured-art"), dao.artistCoverArts(sourceId, "ar-featured")) + assertEquals(listOf("featured-art"), dao.artistCoverArts(sourceId, "ar-guest")) + } + @Test fun downloadedArtistArtUsesOnlyDownloadedAlbums() = runTest {