library: reconcile artist credits with playback and downloads

Left-join albums in the artist queue queries so albumless credited songs can
be played, downloaded and deleted. Add sortName to the starred index, persist
a per-source stale-credit marker so an interrupted sync cannot strand artist
counts or hide artists, and recompute artist play data in one grouped pass.
This commit is contained in:
austinried
2026-10-18 06:42:40 +00:00
parent c64cdd5172
commit 9d9550be48
10 changed files with 2032 additions and 34 deletions
@@ -64,6 +64,8 @@ class MigrationsTest {
assertEquals(1, indexColumn(migrated, "index_albums_starred", "starred").descending)
assertEquals(1, indexColumn(migrated, "index_albums_frequent", "playCount").descending)
assertEquals(1, indexColumn(migrated, "index_albums_recent", "played").descending)
assertEquals("NOCASE", indexColumn(migrated, "index_songs_starred", "sortName").collation)
assertEquals(1, indexColumn(migrated, "index_songs_starred", "starred").descending)
assertEquals("NOCASE", indexColumn(migrated, "index_artists_name", "sortName").collation)
assertEquals(1, indexColumn(migrated, "index_artists_albumCount", "albumCount").descending)
assertEquals(1, indexColumn(migrated, "index_artists_starred", "starred").descending)
@@ -229,6 +231,13 @@ class MigrationsTest {
"INSERT INTO album_artists (sourceId, albumId, position, artistId, name) " +
"VALUES (1, 'al1', 0, 'a1', 'Alpha')",
)
migrated.execSQL(
"INSERT INTO songs (sourceId, id, albumId, artistId, title) VALUES (1, 's1', 'al1', 'a1', 'Song')",
)
migrated.execSQL(
"INSERT INTO song_artists (sourceId, songId, position, artistId, name) " +
"VALUES (1, 's1', 0, 'a1', 'Alpha')",
)
MIGRATION_31_32.migrate(migrated)
@@ -1167,6 +1167,33 @@ class SyncServiceTest {
)
}
@Test
fun aStaleCreditMarkerForcesARecomputeOnTheNextSync() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", "A")),
albums = listOf(album("al1")),
songs = listOf(song("s1")),
)
SyncService(db, source).sync()
assertEquals(false, db.sourcesDao().creditsStale(1))
db.sourcesDao().setCreditsStale(1, true)
db.libraryDao().upsertArtists(
listOf(db.libraryDao().artistOnce(1, "a1")!!.copy(songCount = 0, credited = false)),
)
SyncService(db, source).sync()
val artist = db.libraryDao().artistOnce(1, "a1")!!
assertEquals(1L, artist.songCount)
assertEquals(true, artist.credited)
assertEquals(false, db.sourcesDao().creditsStale(1))
}
@Test
fun everyAlbumArtistAndTrackArtistReachesTheSharedAlbum() =
runTest {
@@ -1251,6 +1278,9 @@ class SyncServiceTest {
SyncService(db, source).sync()
assertEquals(2L, db.libraryDao().artistOnce(1, "a1")!!.songCount)
assertEquals(listOf("s1", "s2"), db.queueDao().artistSongIds(1, "a1"))
assertEquals(2L, db.queueDao().artistLength(1, "a1"))
assertEquals(2, db.queueDao().artistSongs(1, "a1", 0, 10).size)
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed))