mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
library: align artist counts with credited songs
Artist badges and play rankings only counted album credits, while the artist page, shuffle queue and download attribution already included track credits. Derive albumCount and the artist plays from the same four-way credit union, count lead ids that the server omits from the plural refs, and bump the song's credited artists on a local scrobble even when the track has no album. Sort the downloaded artist list by the downloaded album count it displays instead of the synced total, and backfill the artist totals on upgrade so the fix applies before the next sync.
This commit is contained in:
@@ -182,8 +182,10 @@ class LibraryDaoTest {
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", starred = null).copy(albumId = "al-1", playCount = 3, played = 100),
|
||||
song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2", playCount = 4, played = 200),
|
||||
song(sourceId, "s3", "Three", starred = null).copy(albumId = "al-2", playCount = 5, played = 400),
|
||||
song(sourceId, "s2", "Two", starred = null)
|
||||
.copy(albumId = "al-2", artistId = "ar-2", playCount = 4, played = 200),
|
||||
song(sourceId, "s3", "Three", starred = null)
|
||||
.copy(albumId = "al-2", artistId = "ar-2", playCount = 5, played = 400),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -607,7 +609,7 @@ class LibraryDaoTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedFilterShowsDownloadedAlbumCountsButSortsByTheSyncedCount() =
|
||||
fun downloadedFilterSortsByTheDownloadedAlbumCountItShows() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
@@ -635,11 +637,11 @@ class LibraryDaoTest {
|
||||
db.downloadDao().upsert(SongDownload(sourceId, it, DownloadStatus.Completed))
|
||||
}
|
||||
|
||||
// ar-1 has two downloaded albums, ar-2 one. The list still orders by the synced count
|
||||
// (so the album-count index stays usable) but reports the downloaded count.
|
||||
// ar-1 has two downloaded albums, ar-2 one, so the downloaded order is ar-1 first even
|
||||
// though the synced counts would put ar-2 first.
|
||||
val filtered = dao.artistsByAlbumCount(sourceId, 0, "", downloadedFilter = 1).page()
|
||||
assertEquals(listOf("ar-2", "ar-1"), filtered.map { it.id })
|
||||
assertEquals(listOf(1L, 2L), filtered.map { it.albumCount })
|
||||
assertEquals(listOf("ar-1", "ar-2"), filtered.map { it.id })
|
||||
assertEquals(listOf(2L, 1L), filtered.map { it.albumCount })
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -272,6 +272,67 @@ class MigrationsTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theCreditTotalsBackfillUsesEveryCreditedSong() =
|
||||
runTest {
|
||||
driver.open(":memory:").use { migrated ->
|
||||
applySchema(migrated, 33)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (1, 's', 'http://x', 1, 0)",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (2, 't', 'http://y', 1, 0)",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
|
||||
"VALUES (1, 'a1', 'Album Artist', 1, 0, 0, NULL)",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
|
||||
"VALUES (1, 'a2', 'Guest', 0, 0, 0, NULL)",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO artists (sourceId, id, name, albumCount, songCount, playCount, played) " +
|
||||
"VALUES (2, 'b1', 'Unsynced', 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 album_artists (sourceId, albumId, position, artistId, name) " +
|
||||
"VALUES (1, 'al1', 0, 'a1', 'Album Artist')",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO songs (sourceId, id, albumId, artistId, title, playCount, played) " +
|
||||
"VALUES (1, 's1', 'al1', 'a1', 'One', 4, 200)",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO song_artists (sourceId, songId, position, artistId, name) " +
|
||||
"VALUES (1, 's1', 0, 'a1', 'Album Artist')",
|
||||
)
|
||||
migrated.execSQL(
|
||||
"INSERT INTO song_artists (sourceId, songId, position, artistId, name) " +
|
||||
"VALUES (1, 's1', 1, 'a2', 'Guest')",
|
||||
)
|
||||
|
||||
MIGRATION_33_34.migrate(migrated)
|
||||
|
||||
assertEquals(1L, long(migrated, "SELECT songCount FROM artists WHERE id = 'a1'"))
|
||||
assertEquals(1L, long(migrated, "SELECT albumCount FROM artists WHERE id = 'a1'"))
|
||||
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(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'"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun text(
|
||||
connection: SQLiteConnection,
|
||||
sql: String,
|
||||
|
||||
@@ -1465,6 +1465,137 @@ class SyncServiceTest {
|
||||
assertEquals(500L, db.libraryDao().artistOnce(1, "a2")!!.played)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trackOnlyCreditsReachTheArtistBadgeAndPlays() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1", "Album Artist"), artist("a2", "Guest").copy(albumCount = 7)),
|
||||
albums =
|
||||
listOf(
|
||||
album("al1").copy(
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a1", "Album Artist")),
|
||||
),
|
||||
),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(
|
||||
albumId = "al1",
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a1", "Album Artist"), ArtistRef("a2", "Guest")),
|
||||
playCount = 3,
|
||||
played = 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
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(
|
||||
listOf("al1"),
|
||||
db
|
||||
.libraryDao()
|
||||
.albumsForArtist(1, "a2")
|
||||
.first()
|
||||
.map { it.id },
|
||||
)
|
||||
assertEquals(
|
||||
db
|
||||
.libraryDao()
|
||||
.albumsForArtist(1, "a2")
|
||||
.first()
|
||||
.size
|
||||
.toLong(),
|
||||
db
|
||||
.libraryDao()
|
||||
.artistsByName(1, 0, "")
|
||||
.allRows()
|
||||
.first { it.id == "a2" }
|
||||
.albumCount,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumlessTrackCreditsCountAndBumpOnPlay() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1", "Album Artist"), artist("a2", "Guest")),
|
||||
albums = emptyList(),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(
|
||||
albumId = null,
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a1", "Album Artist"), ArtistRef("a2", "Guest")),
|
||||
playCount = 2,
|
||||
played = 50,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
val guest = db.libraryDao().artistOnce(1, "a2")!!
|
||||
assertEquals(0L, guest.albumCount)
|
||||
assertEquals(1L, guest.songCount)
|
||||
assertEquals(2L, guest.playCount)
|
||||
assertEquals(50L, guest.played)
|
||||
|
||||
db.libraryDao().recordPlay(1, "s1", 300)
|
||||
|
||||
assertEquals(3L, db.libraryDao().artistOnce(1, "a2")!!.playCount)
|
||||
assertEquals(300L, db.libraryDao().artistOnce(1, "a2")!!.played)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leadIdsOutsideThePluralRefsKeepArtistTotalsAndShufflesAligned() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1", "Lead"), artist("a2", "Ref")),
|
||||
albums =
|
||||
listOf(
|
||||
album("al1").copy(
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a2", "Ref")),
|
||||
),
|
||||
),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(
|
||||
albumId = "al1",
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a2", "Ref")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
val lead = db.libraryDao().artistOnce(1, "a1")!!
|
||||
assertEquals(1L, lead.songCount)
|
||||
assertEquals(1L, lead.albumCount)
|
||||
assertEquals(db.queueDao().artistLength(1, "a1"), lead.songCount)
|
||||
assertEquals(
|
||||
lead.songCount,
|
||||
db
|
||||
.downloadDao()
|
||||
.artistStatus(1, "a1")
|
||||
.first()
|
||||
.total,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun totalChanges(): Long =
|
||||
db.useWriterConnection { connection ->
|
||||
connection.usePrepared("SELECT total_changes()") { statement ->
|
||||
|
||||
Reference in New Issue
Block a user