mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
queue: preserve ambiguous legacy reset identity
This commit is contained in:
@@ -505,8 +505,8 @@ class QueueRepository(
|
|||||||
upNextAnchor: Long,
|
upNextAnchor: Long,
|
||||||
) {
|
) {
|
||||||
invalidateLibraryCache()
|
invalidateLibraryCache()
|
||||||
writeLocked(withOriginEntryIds(entries))
|
writeLocked(withOriginEntryIds(entries, assignFragmentIds = false))
|
||||||
writeUpNextLocked(withOriginEntryIds(upNext))
|
writeUpNextLocked(withOriginEntryIds(upNext, assignFragmentIds = false))
|
||||||
setShuffleLocked(shuffleSeed)
|
setShuffleLocked(shuffleSeed)
|
||||||
cursorMutex.withLock {
|
cursorMutex.withLock {
|
||||||
dao.setCursor(cursorRow().copy(queuePosition = 0, positionMs = 0, upNextAnchor = upNextAnchor))
|
dao.setCursor(cursorRow().copy(queuePosition = 0, positionMs = 0, upNextAnchor = upNextAnchor))
|
||||||
@@ -588,7 +588,9 @@ class QueueRepository(
|
|||||||
val before = snapshot()
|
val before = snapshot()
|
||||||
val entries = canonicalEntries(before.entries.map { it.entry }, changed)
|
val entries = canonicalEntries(before.entries.map { it.entry }, changed)
|
||||||
val upNext = canonicalEntries(before.upNext.map { it.entry }, changed)
|
val upNext = canonicalEntries(before.upNext.map { it.entry }, changed)
|
||||||
if (entries == before.entries.map { it.entry } && upNext == before.upNext.map { it.entry }) return@withMutationLock
|
val matchingOrigin =
|
||||||
|
before.entries.any { it.entry.origin in changed } || before.upNext.any { it.entry.origin in changed }
|
||||||
|
if (!matchingOrigin) return@withMutationLock
|
||||||
val updated = writeResetLocked(before, entries, upNext, point)
|
val updated = writeResetLocked(before, entries, upNext, point)
|
||||||
resetListener?.invoke(this, QueueReset(point, updated))
|
resetListener?.invoke(this, QueueReset(point, updated))
|
||||||
}
|
}
|
||||||
@@ -856,8 +858,14 @@ class QueueRepository(
|
|||||||
} else {
|
} else {
|
||||||
resolverFor(entry.kind).rank(dao, entry, item.songId) ?: fallbackRank(entry, resolved.length, item.songId)
|
resolverFor(entry.kind).rank(dao, entry, item.songId) ?: fallbackRank(entry, resolved.length, item.songId)
|
||||||
}
|
}
|
||||||
if (rank != null && rank >= entry.offset && rank < entry.offset + resolved.length) {
|
if (rank != null) {
|
||||||
val flat = group.starts[index] + rank - entry.offset
|
val insideEntry = rank >= entry.offset && rank < entry.offset + resolved.length
|
||||||
|
val flat =
|
||||||
|
when {
|
||||||
|
insideEntry -> group.starts[index] + rank - entry.offset
|
||||||
|
entry.originEntryId == null -> rank
|
||||||
|
else -> continue
|
||||||
|
}
|
||||||
if (item.upNext) return snapshot.anchorPlay + 1 + flat
|
if (item.upNext) return snapshot.anchorPlay + 1 + flat
|
||||||
val play = snapshot.playContext(flat) ?: return null
|
val play = snapshot.playContext(flat) ?: return null
|
||||||
return snapshot.combined(play)
|
return snapshot.combined(play)
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
|||||||
import androidx.sqlite.execSQL
|
import androidx.sqlite.execSQL
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.subtracks.data.model.PlaylistSong
|
||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
import com.subtracks.data.model.QueueOrigin
|
import com.subtracks.data.model.QueueOrigin
|
||||||
import com.subtracks.data.repo.QueueRepository
|
import com.subtracks.data.repo.QueueRepository
|
||||||
|
import com.subtracks.data.repo.removeAt
|
||||||
|
import com.subtracks.data.repo.replace
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -524,24 +527,100 @@ class MigrationsTest {
|
|||||||
.build()
|
.build()
|
||||||
try {
|
try {
|
||||||
val repository = QueueRepository(db)
|
val repository = QueueRepository(db)
|
||||||
val before = repository.snapshot()
|
|
||||||
val point = repository.captureResetPoint()
|
val point = repository.captureResetPoint()
|
||||||
|
|
||||||
repository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), point)
|
repository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), point)
|
||||||
|
|
||||||
val after = repository.snapshot()
|
val after = repository.snapshot()
|
||||||
assertEquals(4, after.entries.size)
|
assertEquals(4, after.entries.size)
|
||||||
assertEquals(before.entries, after.entries)
|
assertEquals(listOf(null, null, null, null), after.entries.map { it.entry.originEntryId })
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("s1", "s3", "s1", "s3"),
|
listOf("s1", "s3", "s1", "s3"),
|
||||||
(0 until after.size).map { repository.itemAt(after, it)?.song?.id },
|
(0 until after.size).map { repository.itemAt(after, it)?.song?.id },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
repository.removeAt(after, 0)
|
||||||
|
repository.replace(after.entries.map { it.entry })
|
||||||
|
val undoPoint = repository.captureResetPoint()
|
||||||
|
repository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), undoPoint)
|
||||||
|
|
||||||
|
val undone = repository.snapshot()
|
||||||
|
assertEquals(4, undone.entries.size)
|
||||||
|
assertEquals(
|
||||||
|
listOf("s1", "s3", "s1", "s3"),
|
||||||
|
(0 until undone.size).map { repository.itemAt(undone, it)?.song?.id },
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
db.close()
|
db.close()
|
||||||
context.deleteDatabase(name)
|
context.deleteDatabase(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aRetainedMigratedOriginStillRemapsAndPublishesWhenItsRowsChange() =
|
||||||
|
runTest {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val name = "migration-38-remap-${System.nanoTime()}.db"
|
||||||
|
databaseFile(context, name).parentFile?.mkdirs()
|
||||||
|
driver.open(databaseFile(context, name).absolutePath).use { migrated ->
|
||||||
|
applySchema(migrated, 38)
|
||||||
|
migrated.execSQL("PRAGMA user_version = 38")
|
||||||
|
migrated.execSQL(
|
||||||
|
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (1, 'server', 'http://localhost', 1, 0)",
|
||||||
|
)
|
||||||
|
migrated.execSQL(
|
||||||
|
"INSERT INTO songs (sourceId, id, title) VALUES " +
|
||||||
|
"(1, 's0', 'Zero'), (1, 's1', 'One'), (1, 's2', 'Two'), (1, 's3', 'Three'), (1, 's4', 'Four')",
|
||||||
|
)
|
||||||
|
migrated.execSQL(
|
||||||
|
"INSERT INTO playlist_songs (sourceId, playlistId, songId, position) VALUES " +
|
||||||
|
"(1, 'p1', 's1', 0), (1, 'p1', 's2', 1), (1, 'p1', 's3', 2), (1, 'p1', 's4', 3)",
|
||||||
|
)
|
||||||
|
migrated.execSQL(
|
||||||
|
"INSERT INTO queue_entries (position, sourceId, kind, refId, rangeStart, rangeEnd) VALUES " +
|
||||||
|
"(0, 1, 'Playlist', 'p1', 0, 1), (1, 1, 'Playlist', 'p1', 2, 3)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val db =
|
||||||
|
Room
|
||||||
|
.databaseBuilder<SubtracksDatabase>(context, name)
|
||||||
|
.setDriver(driver)
|
||||||
|
.addMigrations(MIGRATION_38_39)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
val repository = QueueRepository(db)
|
||||||
|
repository.setCursor(2)
|
||||||
|
val point = repository.captureResetPoint()
|
||||||
|
var resets = 0
|
||||||
|
repository.setResetListener { resets++ }
|
||||||
|
db.libraryDao().deletePlaylistSongsFrom(1, "p1", 0)
|
||||||
|
db.libraryDao().upsertPlaylistSongs(
|
||||||
|
listOf(
|
||||||
|
PlaylistSong(1, "p1", "s0", 0),
|
||||||
|
PlaylistSong(1, "p1", "s1", 1),
|
||||||
|
PlaylistSong(1, "p1", "s2", 2),
|
||||||
|
PlaylistSong(1, "p1", "s3", 3),
|
||||||
|
PlaylistSong(1, "p1", "s4", 4),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
repository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), point)
|
||||||
|
|
||||||
|
val snapshot = repository.snapshot()
|
||||||
|
assertEquals(1, resets)
|
||||||
|
assertEquals(3L, repository.cursor())
|
||||||
|
assertEquals("s3", repository.itemAt(snapshot, repository.cursor())?.song?.id)
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
context.deleteDatabase(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun databaseFile(
|
||||||
|
context: Context,
|
||||||
|
name: String,
|
||||||
|
): File = context.getDatabasePath(name)
|
||||||
|
|
||||||
private fun text(
|
private fun text(
|
||||||
connection: SQLiteConnection,
|
connection: SQLiteConnection,
|
||||||
sql: String,
|
sql: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user