library: count credited albums from album credits too

The artist badge now counts albums from album lead ids and album_artists even
when an album has no songs, and only counts song-credited albums that still
exist, matching what albumsForArtist lists. Preserve the derived albumCount
when the server re-upserts an artist row so an identical sync does not rewrite
it twice, relax the migration guard to either relation table, and share one
album-count order between the plain and downloaded artist lists.
This commit is contained in:
austinried
2026-10-18 14:40:16 +00:00
parent e0cfba489e
commit 65d74faa1d
5 changed files with 158 additions and 47 deletions
@@ -283,6 +283,9 @@ class MigrationsTest {
migrated.execSQL(
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (2, 't', 'http://y', 1, 0)",
)
migrated.execSQL(
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (3, 'u', 'http://z', 1, 0)",
)
migrated.execSQL(
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
"VALUES (1, 'a1', 'Album Artist', 1, 0, 0, NULL)",
@@ -295,10 +298,18 @@ class MigrationsTest {
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
"VALUES (2, 'b1', 'Unsynced', 7, 6, 5, 100)",
)
migrated.execSQL(
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
"VALUES (3, 'c1', 'Track Only', 7, 6, 5, 100)",
)
migrated.execSQL(
"INSERT INTO albums (sourceId, id, artistId, name, albumArtist, created, songCount) " +
"VALUES (1, 'al1', 'a1', 'Album', 'Album Artist', 0, 1)",
)
migrated.execSQL(
"INSERT INTO albums (sourceId, id, artistId, name, albumArtist, created, songCount) " +
"VALUES (1, 'al2', 'a2', 'Empty Album', 'Guest', 0, 0)",
)
migrated.execSQL(
"INSERT INTO album_artists (sourceId, albumId, position, artistId, name) " +
"VALUES (1, 'al1', 0, 'a1', 'Album Artist')",
@@ -315,6 +326,18 @@ class MigrationsTest {
"INSERT INTO song_artists (sourceId, songId, position, artistId, name) " +
"VALUES (1, 's1', 1, 'a2', 'Guest')",
)
migrated.execSQL(
"INSERT INTO songs (sourceId, id, albumId, artistId, title, playCount, played) " +
"VALUES (3, 's3', NULL, 'c1', 'Three', 2, 50)",
)
migrated.execSQL(
"INSERT INTO song_artists (sourceId, songId, position, artistId, name) " +
"VALUES (3, 's3', 0, 'c1', 'Track Only')",
)
migrated.execSQL(
"INSERT INTO songs (sourceId, id, albumId, artistId, title, playCount, played) " +
"VALUES (3, 's4', 'missing', 'c1', 'Four', 3, 60)",
)
MIGRATION_33_34.migrate(migrated)
@@ -323,13 +346,17 @@ class MigrationsTest {
assertEquals(4L, long(migrated, "SELECT playCount FROM artists WHERE id = 'a1'"))
assertEquals(200L, long(migrated, "SELECT played FROM artists WHERE id = 'a1'"))
assertEquals(1L, long(migrated, "SELECT songCount FROM artists WHERE id = 'a2'"))
assertEquals(1L, long(migrated, "SELECT albumCount FROM artists WHERE id = 'a2'"))
assertEquals(2L, long(migrated, "SELECT albumCount FROM artists WHERE id = 'a2'"))
assertEquals(4L, long(migrated, "SELECT playCount FROM artists WHERE id = 'a2'"))
assertEquals(200L, long(migrated, "SELECT played FROM artists WHERE id = 'a2'"))
assertEquals(6L, long(migrated, "SELECT songCount FROM artists WHERE id = 'b1'"))
assertEquals(7L, long(migrated, "SELECT albumCount FROM artists WHERE id = 'b1'"))
assertEquals(5L, long(migrated, "SELECT playCount FROM artists WHERE id = 'b1'"))
assertEquals(100L, long(migrated, "SELECT played FROM artists WHERE id = 'b1'"))
assertEquals(2L, long(migrated, "SELECT songCount FROM artists WHERE id = 'c1'"))
assertEquals(0L, long(migrated, "SELECT albumCount FROM artists WHERE id = 'c1'"))
assertEquals(5L, long(migrated, "SELECT playCount FROM artists WHERE id = 'c1'"))
assertEquals(60L, long(migrated, "SELECT played FROM artists WHERE id = 'c1'"))
}
}
@@ -1373,6 +1373,11 @@ class SyncServiceTest {
insertSource()
val source =
FakeMusicSource(
artists =
listOf(
artist("a1", "Artist").copy(albumCount = 9),
artist("a2", "Other").copy(albumCount = 9),
),
albums =
listOf(
album("al1").copy(artists = listOf(ArtistRef("a1", "Artist"), ArtistRef("a2", "Other"))),
@@ -1478,6 +1483,10 @@ class SyncServiceTest {
artistId = "a1",
artists = listOf(ArtistRef("a1", "Album Artist")),
),
album("al2").copy(
artistId = "a2",
artists = listOf(ArtistRef("a2", "Guest")),
),
),
songs =
listOf(
@@ -1488,31 +1497,28 @@ class SyncServiceTest {
playCount = 3,
played = 100,
),
song("s2").copy(
albumId = "al1",
artistId = "a2",
artists = listOf(ArtistRef("a2", "Guest")),
playCount = 4,
played = 200,
),
),
)
SyncService(db, source).sync()
val guest = db.libraryDao().artistOnce(1, "a2")!!
assertEquals(1L, guest.albumCount)
assertEquals(1L, guest.songCount)
assertEquals(3L, guest.playCount)
assertEquals(100L, guest.played)
assertEquals(2L, guest.albumCount)
assertEquals(2L, guest.songCount)
assertEquals(7L, guest.playCount)
assertEquals(200L, guest.played)
val albums = db.libraryDao().albumsForArtist(1, "a2").first()
assertEquals(listOf("al1", "al2"), albums.map { it.id }.sorted())
assertEquals(albums.size.toLong(), guest.albumCount)
assertEquals(
listOf("al1"),
db
.libraryDao()
.albumsForArtist(1, "a2")
.first()
.map { it.id },
)
assertEquals(
db
.libraryDao()
.albumsForArtist(1, "a2")
.first()
.size
.toLong(),
albums.size.toLong(),
db
.libraryDao()
.artistsByName(1, 0, "")
@@ -1520,6 +1526,40 @@ class SyncServiceTest {
.first { it.id == "a2" }
.albumCount,
)
assertEquals(guest.songCount, db.queueDao().artistLength(1, "a2"))
}
@Test
fun songsOnMissingAlbumsAreSongsButNotAlbums() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", "Artist")),
albums = emptyList(),
songs =
listOf(
song("s1").copy(
albumId = "missing",
artistId = "a1",
artists = listOf(ArtistRef("a1", "Artist")),
),
),
)
SyncService(db, source).sync()
val artist = db.libraryDao().artistOnce(1, "a1")!!
assertEquals(1L, artist.songCount)
assertEquals(0L, artist.albumCount)
assertEquals(
emptyList<String>(),
db
.libraryDao()
.albumsForArtist(1, "a1")
.first()
.map { it.id },
)
}
@Test