mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
store the shuffle order in a table with a cache; keep previous at the start unless repeating
This commit is contained in:
+32
-6
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "1fd6e47da427e7d9faceae1bf0774007",
|
||||
"identityHash": "409a9b0fb3a36dbda79d86b64c9656c3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "sources",
|
||||
@@ -704,7 +704,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "playback_cursor",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `queuePosition` INTEGER NOT NULL, `shuffleEnabled` INTEGER NOT NULL DEFAULT 0, `repeatMode` INTEGER NOT NULL DEFAULT 0, `shuffleOrder` TEXT, PRIMARY KEY(`id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `queuePosition` INTEGER NOT NULL, `shuffleEnabled` INTEGER NOT NULL DEFAULT 0, `repeatMode` INTEGER NOT NULL DEFAULT 0, `shuffleSeed` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -733,9 +733,11 @@
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "shuffleOrder",
|
||||
"columnName": "shuffleOrder",
|
||||
"affinity": "TEXT"
|
||||
"fieldPath": "shuffleSeed",
|
||||
"columnName": "shuffleSeed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -745,6 +747,30 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "shuffle_order",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence` INTEGER NOT NULL, `flatPosition` INTEGER NOT NULL, PRIMARY KEY(`sequence`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sequence",
|
||||
"columnName": "sequence",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "flatPosition",
|
||||
"columnName": "flatPosition",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sequence"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "artwork_seeds",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cacheKey` TEXT NOT NULL, `primary` INTEGER NOT NULL, `secondary` INTEGER, PRIMARY KEY(`cacheKey`))",
|
||||
@@ -777,7 +803,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1fd6e47da427e7d9faceae1bf0774007')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '409a9b0fb3a36dbda79d86b64c9656c3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,11 @@ val MIGRATION_5_6 =
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL("ALTER TABLE `playback_cursor` ADD COLUMN `shuffleEnabled` INTEGER NOT NULL DEFAULT 0")
|
||||
connection.execSQL("ALTER TABLE `playback_cursor` ADD COLUMN `repeatMode` INTEGER NOT NULL DEFAULT 0")
|
||||
connection.execSQL("ALTER TABLE `playback_cursor` ADD COLUMN `shuffleOrder` TEXT")
|
||||
connection.execSQL("ALTER TABLE `playback_cursor` ADD COLUMN `shuffleSeed` INTEGER NOT NULL DEFAULT 0")
|
||||
connection.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `shuffle_order` (`sequence` INTEGER NOT NULL, " +
|
||||
"`flatPosition` INTEGER NOT NULL, PRIMARY KEY(`sequence`))",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import com.subtracks.data.model.PlaybackCursor
|
||||
import com.subtracks.data.model.QueueEntry
|
||||
import com.subtracks.data.model.ShuffleOrder
|
||||
import com.subtracks.data.model.SongListItem
|
||||
|
||||
private const val ALBUM_SONGS_SQL =
|
||||
@@ -30,6 +31,15 @@ interface QueueDao {
|
||||
@Query("SELECT * FROM queue_entries ORDER BY position, id")
|
||||
suspend fun entries(): List<QueueEntry>
|
||||
|
||||
@Query("SELECT * FROM shuffle_order ORDER BY sequence")
|
||||
suspend fun shuffleOrder(): List<ShuffleOrder>
|
||||
|
||||
@Query("DELETE FROM shuffle_order")
|
||||
suspend fun clearShuffleOrder()
|
||||
|
||||
@Insert
|
||||
suspend fun insertShuffleOrder(entries: List<ShuffleOrder>)
|
||||
|
||||
@Query("SELECT * FROM playback_cursor WHERE id = 1")
|
||||
suspend fun cursor(): PlaybackCursor?
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.QueueEntry
|
||||
import com.subtracks.data.model.QueueKindConverter
|
||||
import com.subtracks.data.model.SearchIndex
|
||||
import com.subtracks.data.model.ShuffleOrder
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.model.SubsonicSource
|
||||
@@ -28,6 +29,7 @@ import com.subtracks.data.model.SubsonicSource
|
||||
SearchIndex::class,
|
||||
QueueEntry::class,
|
||||
PlaybackCursor::class,
|
||||
ShuffleOrder::class,
|
||||
ArtworkSeed::class,
|
||||
],
|
||||
version = 6,
|
||||
|
||||
@@ -210,7 +210,13 @@ data class PlaybackCursor(
|
||||
val queuePosition: Long,
|
||||
@ColumnInfo(defaultValue = "0") val shuffleEnabled: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "0") val repeatMode: Int = 0,
|
||||
val shuffleOrder: String? = null,
|
||||
@ColumnInfo(defaultValue = "0") val shuffleSeed: Long = 0,
|
||||
)
|
||||
|
||||
@Entity(tableName = "shuffle_order")
|
||||
data class ShuffleOrder(
|
||||
@PrimaryKey val sequence: Long,
|
||||
val flatPosition: Long,
|
||||
)
|
||||
|
||||
@Entity(tableName = "artwork_seeds")
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.PlaybackCursor
|
||||
import com.subtracks.data.model.QueueEntry
|
||||
import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.ShuffleOrder
|
||||
import com.subtracks.data.model.SongListItem
|
||||
|
||||
const val QUEUE_CHUNK = 60
|
||||
@@ -29,12 +30,6 @@ data class QueueSnapshot(
|
||||
|
||||
fun flatPosition(sequence: Long): Long? = if (shuffleOrder == null) sequence else shuffleOrder.getOrNull(sequence.toInt())
|
||||
|
||||
fun sequencePosition(flat: Long): Long {
|
||||
val order = shuffleOrder ?: return flat
|
||||
val index = order.indexOfFirst { it == flat }
|
||||
return if (index >= 0) index.toLong() else flat
|
||||
}
|
||||
|
||||
private fun locateFlat(position: Long): Pair<QueueEntry, Long>? {
|
||||
var remaining = position
|
||||
for (resolved in entries) {
|
||||
@@ -60,6 +55,12 @@ class QueueRepository(
|
||||
) {
|
||||
private val dao get() = db.queueDao()
|
||||
|
||||
@Volatile
|
||||
private var cachedShuffleSeed = 0L
|
||||
|
||||
@Volatile
|
||||
private var cachedShuffleOrder: LongArray? = null
|
||||
|
||||
fun playlistEntry(
|
||||
sourceId: Long,
|
||||
playlistId: String,
|
||||
@@ -82,13 +83,8 @@ class QueueRepository(
|
||||
shuffleOrder: LongArray? = null,
|
||||
) {
|
||||
write(entries)
|
||||
dao.setCursor(
|
||||
cursorRow().copy(
|
||||
queuePosition = 0,
|
||||
shuffleEnabled = shuffleOrder != null,
|
||||
shuffleOrder = shuffleOrder?.joinToString(","),
|
||||
),
|
||||
)
|
||||
setShuffle(shuffleOrder != null, shuffleOrder)
|
||||
dao.setCursor(cursorRow().copy(queuePosition = 0))
|
||||
}
|
||||
|
||||
suspend fun snapshot(): QueueSnapshot =
|
||||
@@ -97,9 +93,10 @@ class QueueRepository(
|
||||
val entries = dao.entries().map { ResolvedQueueEntry(it, it.resolvedLength()) }
|
||||
val row = dao.cursor()
|
||||
val order =
|
||||
row?.shuffleOrder?.parseOrder()?.takeIf {
|
||||
row.shuffleEnabled &&
|
||||
it.size.toLong() == entries.sumOf { e -> e.length }
|
||||
if (row?.shuffleEnabled == true) {
|
||||
loadShuffleOrder(row.shuffleSeed, entries.sumOf { it.length })
|
||||
} else {
|
||||
null
|
||||
}
|
||||
QueueSnapshot(entries, order)
|
||||
}
|
||||
@@ -113,10 +110,31 @@ class QueueRepository(
|
||||
suspend fun setShuffle(
|
||||
enabled: Boolean,
|
||||
order: LongArray?,
|
||||
) = dao.setCursor(cursorRow().copy(shuffleEnabled = enabled, shuffleOrder = order?.joinToString(",")))
|
||||
) {
|
||||
val seed = if (enabled && order != null) System.nanoTime() else 0L
|
||||
dao.clearShuffleOrder()
|
||||
if (enabled && order != null) {
|
||||
dao.insertShuffleOrder(order.mapIndexed { index, flat -> ShuffleOrder(index.toLong(), flat) })
|
||||
}
|
||||
dao.setCursor(cursorRow().copy(shuffleEnabled = enabled, shuffleSeed = seed))
|
||||
cachedShuffleSeed = 0
|
||||
cachedShuffleOrder = null
|
||||
}
|
||||
|
||||
suspend fun setRepeat(mode: Int) = dao.setCursor(cursorRow().copy(repeatMode = mode))
|
||||
|
||||
private suspend fun loadShuffleOrder(
|
||||
seed: Long,
|
||||
size: Long,
|
||||
): LongArray? {
|
||||
val cached = cachedShuffleOrder
|
||||
if (seed == cachedShuffleSeed && cached != null && cached.size.toLong() == size) return cached
|
||||
val loaded = dao.shuffleOrder().map { it.flatPosition }.toLongArray()
|
||||
cachedShuffleSeed = seed
|
||||
cachedShuffleOrder = loaded.takeIf { it.size.toLong() == size }
|
||||
return cachedShuffleOrder
|
||||
}
|
||||
|
||||
suspend fun itemAt(
|
||||
snapshot: QueueSnapshot,
|
||||
position: Long,
|
||||
@@ -307,10 +325,3 @@ class QueueRepository(
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseOrder(): LongArray? =
|
||||
if (isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
split(',').mapNotNull { it.toLongOrNull() }.toLongArray().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
@@ -403,7 +403,20 @@ class PlaybackController(
|
||||
val snapshot = snapshot ?: return
|
||||
if (snapshot.size == 0L) return
|
||||
val current = currentPosition() ?: return
|
||||
jumpTo(if (current > 0) current - 1 else snapshot.size - 1)
|
||||
when {
|
||||
current > 0 -> {
|
||||
jumpTo(current - 1)
|
||||
}
|
||||
|
||||
repeatMode == RepeatMode.All -> {
|
||||
jumpTo(snapshot.size - 1)
|
||||
}
|
||||
|
||||
else -> {
|
||||
player.seekTo(0)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cycleRepeat() {
|
||||
|
||||
@@ -689,7 +689,7 @@ class PlaybackControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previousFromTheStartWrapsToTheLastTrack() {
|
||||
fun previousFromTheStartRestartsWithoutRepeat() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
@@ -698,6 +698,27 @@ class PlaybackControllerTest {
|
||||
?.id == "s1"
|
||||
}
|
||||
|
||||
controller.previous()
|
||||
|
||||
assertTrue(handle.operations.contains("seekTo(0)"))
|
||||
assertEquals(
|
||||
"s1",
|
||||
controller.state.value.item
|
||||
?.id,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previousFromTheStartWrapsWithRepeatAll() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s1"
|
||||
}
|
||||
|
||||
controller.cycleRepeat()
|
||||
controller.previous()
|
||||
await {
|
||||
controller.state.value.item
|
||||
|
||||
Reference in New Issue
Block a user