library: unify artist credit totals and backfill song counts

Use songCount as the total for both artist status queries and stop excluding
albumless tracks from download attribution, so the Artists badge and the
artist page agree. Backfill song counts in the 29->30 migration for offline
upgrades, and recompute only when a song moves albums or its credits change
rather than on every play-metadata sync.
This commit is contained in:
austinried
2026-10-18 04:14:29 +00:00
parent ba77b31dc4
commit 891406eab9
7 changed files with 179 additions and 25 deletions
@@ -153,6 +153,30 @@ class MigrationsTest {
}
}
@Test
fun theArtistSongCountBackfillCountsLegacyCredits() =
runTest {
driver.open(":memory:").use { migrated ->
applySchema(migrated, 29)
migrated.execSQL(
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (1, 's', 'http://x', 1, 0)",
)
migrated.execSQL("INSERT INTO artists (sourceId, id, name, albumCount) VALUES (1, 'ar1', 'Album Artist', 1)")
migrated.execSQL("INSERT INTO artists (sourceId, id, name, albumCount) VALUES (1, 'ar2', 'Guest', 0)")
migrated.execSQL(
"INSERT INTO albums (sourceId, id, artistId, name, albumArtist, created, songCount) " +
"VALUES (1, 'al1', 'ar1', 'Album', 'Album Artist', 0, 1)",
)
migrated.execSQL("INSERT INTO songs (sourceId, id, albumId, artistId, title) VALUES (1, 's1', 'al1', 'ar1', 'One')")
migrated.execSQL("INSERT INTO songs (sourceId, id, albumId, artistId, title) VALUES (1, 's2', NULL, 'ar2', 'Two')")
MIGRATION_29_30.migrate(migrated)
assertEquals(1L, long(migrated, "SELECT songCount FROM artists WHERE id = 'ar1'"))
assertEquals(1L, long(migrated, "SELECT songCount FROM artists WHERE id = 'ar2'"))
}
}
private fun text(
connection: SQLiteConnection,
sql: String,
@@ -647,7 +647,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,
),
),
)
}
@@ -999,6 +999,71 @@ class SyncServiceTest {
assertEquals(0L, db.libraryDao().artistOnce(1, "a2")!!.songCount)
}
@Test
fun artistSongCountsAndDownloadsIncludeAlbumlessTracks() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", "A")),
songs =
listOf(
song("s1").copy(albumId = null),
song("s2").copy(albumId = null),
),
)
SyncService(db, source).sync()
assertEquals(2L, db.libraryDao().artistOnce(1, "a1")!!.songCount)
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed))
assertEquals(
ListDownloadStatus(total = 2, downloaded = 2),
db.downloadDao().artistStatus(1, "a1").first(),
)
assertEquals(
2L,
db
.downloadDao()
.artistStatuses(1)
.first()
.single { it.id == "a1" }
.downloaded,
)
}
@Test
fun movingASongBetweenAlbumsUpdatesAlbumArtistCounts() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", "A"), artist("a2", "B"), artist("a3", "Track")),
albums =
listOf(
album("al1").copy(artistId = "a1"),
album("al2").copy(artistId = "a2"),
),
songs = listOf(song("s1").copy(artistId = "a3", albumId = "al1")),
)
SyncService(db, source).sync()
assertEquals(1L, db.libraryDao().artistOnce(1, "a1")!!.songCount)
assertEquals(0L, db.libraryDao().artistOnce(1, "a2")!!.songCount)
assertEquals(1L, db.libraryDao().artistOnce(1, "a3")!!.songCount)
source.songs = listOf(song("s1").copy(artistId = "a3", albumId = "al2"))
SyncService(db, source).sync()
assertEquals(0L, db.libraryDao().artistOnce(1, "a1")!!.songCount)
assertEquals(1L, db.libraryDao().artistOnce(1, "a2")!!.songCount)
assertEquals(1L, db.libraryDao().artistOnce(1, "a3")!!.songCount)
}
@Test
fun anIdenticalSyncDoesNotRewriteArtistRows() =
runTest {