diff --git a/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt b/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt index 0a19b7ac..335fcecd 100644 --- a/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt +++ b/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt @@ -219,6 +219,7 @@ class SyncService( } } connection.upsertChanged("song_artists", songArtistKey, artists) { it.columns() } + connection.recordPlaylistOrigins(batch.filter { previousStates[it.id] == null }.map { it.id }) batch.forEach { song -> val previous = previousStates[song.id] val next = nextStates.getValue(song.id) @@ -242,7 +243,9 @@ class SyncService( ids.forEach { changedOrigins += origin(QueueKind.Song, it) } db.useWriterConnection { connection -> connection.immediateTransaction { - connection.storedSongSyncStates(ids.toList()).values.forEach { recordSongOrigins(it, null) } + val previousStates = connection.storedSongSyncStates(ids.toList()) + connection.recordPlaylistOrigins(previousStates.keys) + previousStates.values.forEach { recordSongOrigins(it, null) } if (connection.completedDownloadIds(ids.toList()).isNotEmpty()) changedOrigins += origin(QueueKind.Downloaded, "") db.libraryDao().deleteSongs(source.id, ids) connection.persistPendingOrigins() @@ -250,18 +253,19 @@ class SyncService( } } + private suspend fun PooledConnection.recordPlaylistOrigins(songIds: Collection) { + readSourceIds( + songIds, + { "SELECT playlistId FROM playlist_songs WHERE sourceId = ? AND songId IN ($it)" }, + ) { statement -> + changedOrigins += origin(QueueKind.Playlist, statement.getText(0)) + } + } + private suspend fun PooledConnection.recordSongOrigins( previous: SongSyncState?, next: SongSyncState?, ) { - if ((previous == null) != (next == null)) { - val songId = (previous ?: next)!!.id - usePrepared("SELECT playlistId FROM playlist_songs WHERE sourceId = ? AND songId = ?") { statement -> - statement.bindLong(1, source.id) - statement.bindText(2, songId) - while (statement.step()) changedOrigins += origin(QueueKind.Playlist, statement.getText(0)) - } - } val states = listOfNotNull(previous, next) states.mapTo(changedOrigins) { origin(QueueKind.Song, it.id) } states.mapNotNullTo(changedOrigins) { it.albumId?.let { id -> origin(QueueKind.Album, id) } } diff --git a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt index 849906c5..8bd87df4 100644 --- a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt @@ -2034,6 +2034,54 @@ class SyncServiceTest { assertTrue(restored.changedOrigins.contains(QueueOrigin(QueueKind.Playlist, 1, "p1"))) } + @Test + fun songPresenceChangesBatchPlaylistMembershipQueries() = + runTest { + val executions = Collections.synchronizedList(mutableListOf()) + val loggingDb = createTestDatabase(LoggingSQLiteDriver(BundledSQLiteDriver()) { executions += it }) + try { + loggingDb.sourcesDao().upsertSource( + Source(id = 1, name = "test", address = "http://localhost", isActive = true, createdAt = 0), + ) + val songs = listOf(song("s1"), song("s2"), song("s3")) + val source = + FakeMusicSource( + songs = songs, + playlists = listOf(playlist("p1")), + playlistSongs = songs.mapIndexed { index, song -> PlaylistSong(1, "p1", song.id, index.toLong()) }, + ) + + fun membershipQueryCount() = + synchronized(executions) { + executions.count { + it.sql.startsWith("SELECT playlistId FROM playlist_songs WHERE sourceId = ? AND songId") + } + } + + SyncService(loggingDb, source).sync() + val initialQueries = membershipQueryCount() + loggingDb.queueDao().clearPendingSyncOrigins(1) + synchronized(executions) { executions.clear() } + + source.songs = emptyList() + val deleted = SyncService(loggingDb, source).sync() + val deletionQueries = membershipQueryCount() + assertTrue(deleted.changedOrigins.contains(QueueOrigin(QueueKind.Playlist, 1, "p1"))) + assertTrue(loggingDb.queueDao().pendingSyncOrigins(1).any { it.kind == QueueKind.Playlist && it.refId == "p1" }) + loggingDb.queueDao().clearPendingSyncOrigins(1) + synchronized(executions) { executions.clear() } + + source.songs = songs + val restored = SyncService(loggingDb, source).sync() + val restorationQueries = membershipQueryCount() + assertTrue(restored.changedOrigins.contains(QueueOrigin(QueueKind.Playlist, 1, "p1"))) + assertTrue(loggingDb.queueDao().pendingSyncOrigins(1).any { it.kind == QueueKind.Playlist && it.refId == "p1" }) + assertEquals(listOf(1, 1, 1), listOf(initialQueries, deletionQueries, restorationQueries)) + } finally { + loggingDb.close() + } + } + @Test fun downloadedSongSortChangesReportDownloadedOrigins() = runTest {