mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
library: store artist song counts for download badges
Add artists.songCount, recomputed after syncs that change albums or songs, and derive the Artists tab status map from the download rows plus that count instead of walking every song. The badge query is now proportional to downloads rather than library size.
This commit is contained in:
+1819
File diff suppressed because it is too large
Load Diff
@@ -47,30 +47,30 @@ internal const val ARTIST_DOWNLOAD_ROWS =
|
||||
|
||||
// Starts from downloads so lists and filters over partially downloaded libraries do not scan songs.
|
||||
internal const val DOWNLOADED_ARTIST_ROWS =
|
||||
"SELECT albums.artistId AS id, song_downloads.downloadedAt AS downloadedAt, " +
|
||||
"song_downloads.status AS status " +
|
||||
"SELECT albums.artistId AS id, song_downloads.songId AS songId, " +
|
||||
"song_downloads.downloadedAt AS downloadedAt, song_downloads.status AS status " +
|
||||
"FROM song_downloads " +
|
||||
"JOIN songs ON songs.sourceId = song_downloads.sourceId AND songs.id = song_downloads.songId " +
|
||||
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE song_downloads.sourceId = :sourceId AND albums.artistId IS NOT NULL " +
|
||||
"UNION ALL " +
|
||||
"SELECT album_artists.artistId AS id, song_downloads.downloadedAt AS downloadedAt, " +
|
||||
"song_downloads.status AS status " +
|
||||
"SELECT album_artists.artistId AS id, song_downloads.songId AS songId, " +
|
||||
"song_downloads.downloadedAt AS downloadedAt, song_downloads.status AS status " +
|
||||
"FROM song_downloads " +
|
||||
"JOIN songs ON songs.sourceId = song_downloads.sourceId AND songs.id = song_downloads.songId " +
|
||||
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"JOIN album_artists ON album_artists.sourceId = albums.sourceId AND album_artists.albumId = albums.id " +
|
||||
"WHERE song_downloads.sourceId = :sourceId " +
|
||||
"UNION ALL " +
|
||||
"SELECT songs.artistId AS id, song_downloads.downloadedAt AS downloadedAt, " +
|
||||
"song_downloads.status AS status " +
|
||||
"SELECT songs.artistId AS id, song_downloads.songId AS songId, " +
|
||||
"song_downloads.downloadedAt AS downloadedAt, song_downloads.status AS status " +
|
||||
"FROM song_downloads " +
|
||||
"JOIN songs ON songs.sourceId = song_downloads.sourceId AND songs.id = song_downloads.songId " +
|
||||
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE song_downloads.sourceId = :sourceId AND songs.artistId IS NOT NULL " +
|
||||
"UNION ALL " +
|
||||
"SELECT song_artists.artistId AS id, song_downloads.downloadedAt AS downloadedAt, " +
|
||||
"song_downloads.status AS status " +
|
||||
"SELECT song_artists.artistId AS id, song_downloads.songId AS songId, " +
|
||||
"song_downloads.downloadedAt AS downloadedAt, song_downloads.status AS status " +
|
||||
"FROM song_downloads " +
|
||||
"JOIN songs ON songs.sourceId = song_downloads.sourceId AND songs.id = song_downloads.songId " +
|
||||
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
@@ -140,15 +140,15 @@ interface DownloadDao {
|
||||
fun albumStatuses(sourceId: Long): Flow<List<EntityDownloadStatus>>
|
||||
|
||||
@Query(
|
||||
"SELECT id, " +
|
||||
"COUNT(DISTINCT songId) AS total, " +
|
||||
"SELECT statuses.id, artists.songCount AS total, statuses.downloaded, statuses.downloading " +
|
||||
"FROM (SELECT id, " +
|
||||
"COUNT(DISTINCT CASE WHEN status = 'Completed' THEN songId END) AS downloaded, " +
|
||||
"COUNT(DISTINCT CASE WHEN status IN ('Queued', 'Running') THEN songId END) AS downloading " +
|
||||
"FROM (" + ARTIST_DOWNLOAD_ROWS + ") " +
|
||||
"WHERE id IN (SELECT id FROM (" + DOWNLOADED_ARTIST_ROWS + ") " +
|
||||
"WHERE status IN ('Completed', 'Queued', 'Running')) " +
|
||||
"GROUP BY id " +
|
||||
"HAVING downloaded > 0 OR downloading > 0",
|
||||
"FROM (" + DOWNLOADED_ARTIST_ROWS + ") " +
|
||||
"WHERE status IN ('Completed', 'Queued', 'Running') " +
|
||||
"GROUP BY id) statuses " +
|
||||
"JOIN artists ON artists.sourceId = :sourceId AND artists.id = statuses.id " +
|
||||
"WHERE statuses.downloaded > 0 OR statuses.downloading > 0",
|
||||
)
|
||||
fun artistStatuses(sourceId: Long): Flow<List<EntityDownloadStatus>>
|
||||
|
||||
|
||||
@@ -225,6 +225,26 @@ interface LibraryDao {
|
||||
)
|
||||
suspend fun recomputeArtistPlayData(sourceId: Long)
|
||||
|
||||
// Stored so the download badge query can report an artist's total without walking every song.
|
||||
@Query(
|
||||
"WITH credited AS (" +
|
||||
"SELECT artistId, songId FROM song_artists WHERE sourceId = :sourceId " +
|
||||
"UNION " +
|
||||
"SELECT album_artists.artistId AS artistId, songs.id AS songId " +
|
||||
"FROM album_artists " +
|
||||
"JOIN songs ON songs.sourceId = album_artists.sourceId AND songs.albumId = album_artists.albumId " +
|
||||
"WHERE album_artists.sourceId = :sourceId" +
|
||||
"), counts AS (" +
|
||||
"SELECT artistId AS id, COUNT(*) AS songCount FROM credited GROUP BY artistId" +
|
||||
") " +
|
||||
"UPDATE artists SET songCount = c.songCount " +
|
||||
"FROM (SELECT artists.id AS id, COALESCE(counts.songCount, 0) AS songCount " +
|
||||
"FROM artists LEFT JOIN counts ON counts.id = artists.id " +
|
||||
"WHERE artists.sourceId = :sourceId) c " +
|
||||
"WHERE artists.sourceId = :sourceId AND artists.id = c.id AND artists.songCount != c.songCount",
|
||||
)
|
||||
suspend fun recomputeArtistSongCounts(sourceId: Long)
|
||||
|
||||
@Transaction
|
||||
suspend fun recomputePlayData(sourceId: Long) {
|
||||
recomputeAlbumPlayData(sourceId)
|
||||
|
||||
@@ -422,6 +422,13 @@ val MIGRATION_28_29 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_29_30 =
|
||||
object : Migration(29, 30) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL("ALTER TABLE `artists` ADD COLUMN `songCount` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities.
|
||||
private suspend fun createSearchIndex(
|
||||
connection: SQLiteConnection,
|
||||
@@ -484,4 +491,5 @@ val MIGRATIONS: Array<Migration> =
|
||||
MIGRATION_26_27,
|
||||
MIGRATION_27_28,
|
||||
MIGRATION_28_29,
|
||||
MIGRATION_29_30,
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ import com.subtracks.data.model.UpNextEntry
|
||||
SongDownload::class,
|
||||
PendingAction::class,
|
||||
],
|
||||
version = 29,
|
||||
version = 30,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(
|
||||
|
||||
@@ -89,6 +89,7 @@ data class Artist(
|
||||
val coverArt: String? = null,
|
||||
@ColumnInfo(defaultValue = "0") val playCount: Long = 0,
|
||||
val played: Long? = null,
|
||||
@ColumnInfo(defaultValue = "0") val songCount: Long = 0,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
|
||||
@@ -33,9 +33,12 @@ class SyncService(
|
||||
|
||||
suspend fun sync(): SyncSummary {
|
||||
syncArtists()
|
||||
val libraryBefore = totalChanges()
|
||||
syncAlbums()
|
||||
syncSongs()
|
||||
val libraryChanged = totalChanges() != libraryBefore
|
||||
optimize()
|
||||
if (libraryChanged) recomputeArtistSongCounts()
|
||||
aggregatePlayData()
|
||||
syncPlaylists()
|
||||
syncPlaylistSongs()
|
||||
@@ -138,6 +141,18 @@ class SyncService(
|
||||
db.libraryDao().recomputePlayData(source.id)
|
||||
}
|
||||
|
||||
private suspend fun recomputeArtistSongCounts() {
|
||||
db.libraryDao().recomputeArtistSongCounts(source.id)
|
||||
}
|
||||
|
||||
private suspend fun totalChanges(): Long =
|
||||
db.useWriterConnection { connection ->
|
||||
connection.usePrepared("SELECT total_changes()") { statement ->
|
||||
statement.step()
|
||||
statement.getLong(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Without sqlite_stat1 the artist and relation queries can fall back to whole-table scans.
|
||||
private suspend fun optimize() {
|
||||
db.useWriterConnection { connection ->
|
||||
|
||||
@@ -682,7 +682,17 @@ class DownloadRepositoryTest {
|
||||
),
|
||||
)
|
||||
db.libraryDao().upsertArtists(
|
||||
listOf(Artist(sourceId = 1, id = "ar-guest", name = "Guest", albumCount = 0, starred = null, coverArt = null)),
|
||||
listOf(
|
||||
Artist(
|
||||
sourceId = 1,
|
||||
id = "ar-guest",
|
||||
name = "Guest",
|
||||
albumCount = 0,
|
||||
starred = null,
|
||||
coverArt = null,
|
||||
songCount = 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed))
|
||||
}
|
||||
@@ -910,7 +920,9 @@ class DownloadRepositoryTest {
|
||||
),
|
||||
)
|
||||
db.libraryDao().upsertArtists(
|
||||
listOf(Artist(sourceId = 1, id = "ar2", name = "Other", albumCount = 1, starred = null, coverArt = null)),
|
||||
listOf(
|
||||
Artist(sourceId = 1, id = "ar2", name = "Other", albumCount = 1, starred = null, coverArt = null, songCount = 1),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
|
||||
db.downloadDao().upsert(SongDownload(1, "s3", DownloadStatus.Queued))
|
||||
@@ -1166,7 +1178,7 @@ class DownloadRepositoryTest {
|
||||
)
|
||||
|
||||
private fun artist(coverArt: String?) =
|
||||
Artist(sourceId = 1, id = "ar1", name = "Artist", albumCount = 1, starred = null, coverArt = coverArt)
|
||||
Artist(sourceId = 1, id = "ar1", name = "Artist", albumCount = 1, starred = null, coverArt = coverArt, songCount = 2)
|
||||
|
||||
private fun seedLibrary() {
|
||||
runBlocking {
|
||||
|
||||
@@ -979,6 +979,8 @@ class SyncServiceTest {
|
||||
assertEquals(listOf("s1"), db.queueDao().artistSongIds(1, "a2"))
|
||||
assertEquals(1L, db.queueDao().artistLength(1, "a2"))
|
||||
assertEquals("B", db.libraryDao().artistName(1, "a2").first())
|
||||
assertEquals(1L, db.libraryDao().artistOnce(1, "a1")!!.songCount)
|
||||
assertEquals(1L, db.libraryDao().artistOnce(1, "a2")!!.songCount)
|
||||
|
||||
source.albums = listOf(album("al1").copy(artistId = "a1", artists = listOf(ArtistRef("a1", "A"))))
|
||||
source.songs = listOf(song("s1").copy(artistId = "a1", artists = listOf(ArtistRef("a1", "A"))))
|
||||
@@ -994,6 +996,7 @@ class SyncServiceTest {
|
||||
.map { it.id },
|
||||
)
|
||||
assertEquals(emptyList<String>(), db.queueDao().artistSongIds(1, "a2"))
|
||||
assertEquals(0L, db.libraryDao().artistOnce(1, "a2")!!.songCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user