mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +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,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"identityHash": "1fd6e47da427e7d9faceae1bf0774007",
|
"identityHash": "409a9b0fb3a36dbda79d86b64c9656c3",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "sources",
|
"tableName": "sources",
|
||||||
@@ -704,7 +704,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "playback_cursor",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -733,9 +733,11 @@
|
|||||||
"defaultValue": "0"
|
"defaultValue": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "shuffleOrder",
|
"fieldPath": "shuffleSeed",
|
||||||
"columnName": "shuffleOrder",
|
"columnName": "shuffleSeed",
|
||||||
"affinity": "TEXT"
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"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",
|
"tableName": "artwork_seeds",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cacheKey` TEXT NOT NULL, `primary` INTEGER NOT NULL, `secondary` INTEGER, PRIMARY KEY(`cacheKey`))",
|
"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": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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) {
|
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 `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 `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 androidx.room3.Upsert
|
||||||
import com.subtracks.data.model.PlaybackCursor
|
import com.subtracks.data.model.PlaybackCursor
|
||||||
import com.subtracks.data.model.QueueEntry
|
import com.subtracks.data.model.QueueEntry
|
||||||
|
import com.subtracks.data.model.ShuffleOrder
|
||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
|
|
||||||
private const val ALBUM_SONGS_SQL =
|
private const val ALBUM_SONGS_SQL =
|
||||||
@@ -30,6 +31,15 @@ interface QueueDao {
|
|||||||
@Query("SELECT * FROM queue_entries ORDER BY position, id")
|
@Query("SELECT * FROM queue_entries ORDER BY position, id")
|
||||||
suspend fun entries(): List<QueueEntry>
|
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")
|
@Query("SELECT * FROM playback_cursor WHERE id = 1")
|
||||||
suspend fun cursor(): PlaybackCursor?
|
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.QueueEntry
|
||||||
import com.subtracks.data.model.QueueKindConverter
|
import com.subtracks.data.model.QueueKindConverter
|
||||||
import com.subtracks.data.model.SearchIndex
|
import com.subtracks.data.model.SearchIndex
|
||||||
|
import com.subtracks.data.model.ShuffleOrder
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import com.subtracks.data.model.SubsonicSource
|
import com.subtracks.data.model.SubsonicSource
|
||||||
@@ -28,6 +29,7 @@ import com.subtracks.data.model.SubsonicSource
|
|||||||
SearchIndex::class,
|
SearchIndex::class,
|
||||||
QueueEntry::class,
|
QueueEntry::class,
|
||||||
PlaybackCursor::class,
|
PlaybackCursor::class,
|
||||||
|
ShuffleOrder::class,
|
||||||
ArtworkSeed::class,
|
ArtworkSeed::class,
|
||||||
],
|
],
|
||||||
version = 6,
|
version = 6,
|
||||||
|
|||||||
@@ -210,7 +210,13 @@ data class PlaybackCursor(
|
|||||||
val queuePosition: Long,
|
val queuePosition: Long,
|
||||||
@ColumnInfo(defaultValue = "0") val shuffleEnabled: Boolean = false,
|
@ColumnInfo(defaultValue = "0") val shuffleEnabled: Boolean = false,
|
||||||
@ColumnInfo(defaultValue = "0") val repeatMode: Int = 0,
|
@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")
|
@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.PlaybackCursor
|
||||||
import com.subtracks.data.model.QueueEntry
|
import com.subtracks.data.model.QueueEntry
|
||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
|
import com.subtracks.data.model.ShuffleOrder
|
||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
|
|
||||||
const val QUEUE_CHUNK = 60
|
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 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>? {
|
private fun locateFlat(position: Long): Pair<QueueEntry, Long>? {
|
||||||
var remaining = position
|
var remaining = position
|
||||||
for (resolved in entries) {
|
for (resolved in entries) {
|
||||||
@@ -60,6 +55,12 @@ class QueueRepository(
|
|||||||
) {
|
) {
|
||||||
private val dao get() = db.queueDao()
|
private val dao get() = db.queueDao()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cachedShuffleSeed = 0L
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cachedShuffleOrder: LongArray? = null
|
||||||
|
|
||||||
fun playlistEntry(
|
fun playlistEntry(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
playlistId: String,
|
playlistId: String,
|
||||||
@@ -82,13 +83,8 @@ class QueueRepository(
|
|||||||
shuffleOrder: LongArray? = null,
|
shuffleOrder: LongArray? = null,
|
||||||
) {
|
) {
|
||||||
write(entries)
|
write(entries)
|
||||||
dao.setCursor(
|
setShuffle(shuffleOrder != null, shuffleOrder)
|
||||||
cursorRow().copy(
|
dao.setCursor(cursorRow().copy(queuePosition = 0))
|
||||||
queuePosition = 0,
|
|
||||||
shuffleEnabled = shuffleOrder != null,
|
|
||||||
shuffleOrder = shuffleOrder?.joinToString(","),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun snapshot(): QueueSnapshot =
|
suspend fun snapshot(): QueueSnapshot =
|
||||||
@@ -97,9 +93,10 @@ class QueueRepository(
|
|||||||
val entries = dao.entries().map { ResolvedQueueEntry(it, it.resolvedLength()) }
|
val entries = dao.entries().map { ResolvedQueueEntry(it, it.resolvedLength()) }
|
||||||
val row = dao.cursor()
|
val row = dao.cursor()
|
||||||
val order =
|
val order =
|
||||||
row?.shuffleOrder?.parseOrder()?.takeIf {
|
if (row?.shuffleEnabled == true) {
|
||||||
row.shuffleEnabled &&
|
loadShuffleOrder(row.shuffleSeed, entries.sumOf { it.length })
|
||||||
it.size.toLong() == entries.sumOf { e -> e.length }
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
QueueSnapshot(entries, order)
|
QueueSnapshot(entries, order)
|
||||||
}
|
}
|
||||||
@@ -113,10 +110,31 @@ class QueueRepository(
|
|||||||
suspend fun setShuffle(
|
suspend fun setShuffle(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
order: LongArray?,
|
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))
|
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(
|
suspend fun itemAt(
|
||||||
snapshot: QueueSnapshot,
|
snapshot: QueueSnapshot,
|
||||||
position: Long,
|
position: Long,
|
||||||
@@ -307,10 +325,3 @@ class QueueRepository(
|
|||||||
return result
|
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
|
val snapshot = snapshot ?: return
|
||||||
if (snapshot.size == 0L) return
|
if (snapshot.size == 0L) return
|
||||||
val current = currentPosition() ?: 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() {
|
fun cycleRepeat() {
|
||||||
|
|||||||
@@ -689,7 +689,7 @@ class PlaybackControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun previousFromTheStartWrapsToTheLastTrack() {
|
fun previousFromTheStartRestartsWithoutRepeat() {
|
||||||
seedAlbum(3, sourceId = 1)
|
seedAlbum(3, sourceId = 1)
|
||||||
|
|
||||||
controller.playAlbum(1, "al1", 0)
|
controller.playAlbum(1, "al1", 0)
|
||||||
@@ -698,6 +698,27 @@ class PlaybackControllerTest {
|
|||||||
?.id == "s1"
|
?.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()
|
controller.previous()
|
||||||
await {
|
await {
|
||||||
controller.state.value.item
|
controller.state.value.item
|
||||||
|
|||||||
Reference in New Issue
Block a user