From 3e042cb3806214b012179bd9e4ac82d66aca2663 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:08:56 +0000 Subject: [PATCH] download a song's playlist artwork too --- .../com/subtracks/data/db/DownloadDao.kt | 20 ++++++++++++++++ .../com/subtracks/data/model/Download.kt | 5 ++++ .../subtracks/data/repo/DownloadRepository.kt | 20 ++++++++++------ .../data/repo/DownloadRepositoryTest.kt | 23 +++++++++++++++++++ 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt index 5ed2b482..ae7e80cd 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt @@ -8,6 +8,7 @@ import com.subtracks.data.model.DownloadedSong import com.subtracks.data.model.EntityDownloadStatus import com.subtracks.data.model.ListDownloadStatus import com.subtracks.data.model.SongDownload +import com.subtracks.data.model.SourceCoverArt import kotlinx.coroutines.flow.Flow @Dao @@ -125,6 +126,25 @@ interface DownloadDao { ) suspend fun artwork(): List + @Query( + "SELECT DISTINCT p.coverArt AS coverArt FROM playlist_songs ps " + + "JOIN playlists p ON p.sourceId = ps.sourceId AND p.id = ps.playlistId " + + "WHERE ps.sourceId = :sourceId AND ps.songId = :songId AND p.coverArt IS NOT NULL", + ) + suspend fun playlistCoversForSong( + sourceId: Long, + songId: String, + ): List + + @Query( + "SELECT DISTINCT ps.sourceId AS sourceId, p.coverArt AS coverArt " + + "FROM playlist_songs ps " + + "JOIN playlists p ON p.sourceId = ps.sourceId AND p.id = ps.playlistId " + + "JOIN song_downloads d ON d.sourceId = ps.sourceId AND d.songId = ps.songId " + + "WHERE p.coverArt IS NOT NULL", + ) + suspend fun downloadedPlaylistCovers(): List + @Query("SELECT * FROM song_downloads WHERE sourceId = :sourceId AND songId = :songId") suspend fun find( sourceId: Long, diff --git a/app/src/main/kotlin/com/subtracks/data/model/Download.kt b/app/src/main/kotlin/com/subtracks/data/model/Download.kt index cdc03319..1c39d181 100644 --- a/app/src/main/kotlin/com/subtracks/data/model/Download.kt +++ b/app/src/main/kotlin/com/subtracks/data/model/Download.kt @@ -86,6 +86,11 @@ data class DownloadArtwork( get() = (albumId != null && albumRow == null) || (artistId != null && artistRow == null) } +data class SourceCoverArt( + val sourceId: Long, + val coverArt: String, +) + class DownloadStatusConverter { @ColumnTypeConverter fun fromDownloadStatus(status: DownloadStatus): String = status.name diff --git a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt index 9cdb1289..4ad1519e 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt @@ -388,9 +388,11 @@ class DownloadRepository( ) { val song = db.libraryDao().songOnce(sourceId, songId) val coverArt = - listOfNotNull( - song?.albumId?.let { db.libraryDao().albumOnce(sourceId, it)?.coverArt }, - song?.artistId?.let { db.libraryDao().artistOnce(sourceId, it)?.coverArt }, + ( + listOfNotNull( + song?.albumId?.let { db.libraryDao().albumOnce(sourceId, it)?.coverArt }, + song?.artistId?.let { db.libraryDao().artistOnce(sourceId, it)?.coverArt }, + ) + db.downloadDao().playlistCoversForSong(sourceId, songId) ).distinct() coverArt.forEach { cover -> storeArtwork(sourceId, cover, thumbnail = false) @@ -423,18 +425,22 @@ class DownloadRepository( * rows could not all be resolved against the library, in which case nothing is swept for it: * pruning an album or artist would otherwise delete artwork a download still holds. */ - private suspend fun artworkToKeep(): Map?> = - db + private suspend fun artworkToKeep(): Map?> { + val playlistCovers = db.downloadDao().downloadedPlaylistCovers().groupBy({ it.sourceId }, { it.coverArt }) + return db .downloadDao() .artwork() .groupBy { it.sourceId } .mapValues { (sourceId, rows) -> if (rows.any { it.hasMissingLibraryRow }) return@mapValues null - rows - .flatMap { row -> listOfNotNull(row.albumCoverArt, row.artistCoverArt) } + val covers = + rows.flatMap { row -> listOfNotNull(row.albumCoverArt, row.artistCoverArt) } + + playlistCovers[sourceId].orEmpty() + covers .flatMap { cover -> listOf(false, true).map { thumbnail -> coverArtKey(sourceId, cover, thumbnail) } } .toSet() } + } private suspend fun recover(row: SongDownload) { when (row.status) { diff --git a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt index 7105f1be..9565f04a 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt @@ -323,6 +323,28 @@ class DownloadRepositoryTest { } } + @Test + fun downloadingASongAlsoStoresTheArtworkOfPlaylistsThatContainIt() = + runTest { + seedLibrary() + runBlocking { + db.libraryDao().upsertPlaylists( + listOf(Playlist(1, "pl1", "Playlist", comment = null, coverArt = PLAYLIST_ART, songCount = 2, created = 0)), + ) + db.libraryDao().upsertPlaylistSongs(listOf(PlaylistSong(1, "pl1", "s1", 0))) + } + + runBlocking { repository.download(1, "s1") } + + listOf(false, true).forEach { thumbnail -> + val key = coverArtKey(1, PLAYLIST_ART, thumbnail) + assertNotNull("expected stored playlist art for $key", artwork.uri(1, key)) + } + + runBlocking { repository.reconcile() } + assertNotNull(artwork.uri(1, coverArtKey(1, PLAYLIST_ART, false))) + } + @Test fun theDownloadStartsBeforeItsArtworkIsFetched() = runTest { @@ -1105,5 +1127,6 @@ class DownloadRepositoryTest { private companion object { const val ALBUM_ART = "art-al1" const val ARTIST_ART = "art-ar1" + const val PLAYLIST_ART = "art-pl1" } }