mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
library: resolve artist art from live credits
Artist detail only used the materialized artist cover list, so credited track artists without an artists row, or with an empty or stale cover list, always fell back to the letter placeholder even though their appearances albums had art. Observe the credit-derived cover query and use it for the detail art, preferring the cached downloaded covers when offline. The query covers album, album_artists, song and song_artists credits.
This commit is contained in:
@@ -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<String>
|
||||
|
||||
@Query(ARTIST_COVER_ARTS_SQL)
|
||||
fun artistCoverArtsFlow(
|
||||
sourceId: Long,
|
||||
artistId: String,
|
||||
): Flow<List<String>>
|
||||
|
||||
@Query(DOWNLOADED_ARTIST_ART_UPDATE_SQL)
|
||||
suspend fun recomputeDownloadedArtistArt(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -442,6 +442,11 @@ class LibraryRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun artistCoverArts(
|
||||
sourceId: Long,
|
||||
artistId: String,
|
||||
): Flow<List<String>> = db.libraryDao().artistCoverArtsFlow(sourceId, artistId)
|
||||
|
||||
fun albumDiscs(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
|
||||
@@ -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<String>,
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user