Skip to content

Commit 0c067f2

Browse files
committed
refactor: db linked list operators
1 parent d04408b commit 0c067f2

File tree

5 files changed

+242
-53
lines changed

5 files changed

+242
-53
lines changed

app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ abstract class SongQueueSongMappingStore {
4949
@RawQuery
5050
protected abstract fun findByNextIdRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping?
5151

52-
fun findByNextId(queueId: String, nextId: String): Song.AlongSongQueueMapping? {
52+
fun findByNextId(queueId: String, nextId: String?): Song.AlongSongQueueMapping? {
5353
val query = "SELECT ${Song.TABLE}.*, " +
5454
"${SongQueueSongMapping.TABLE}.* " +
5555
"FROM ${SongQueueSongMapping.TABLE} " +
@@ -72,9 +72,43 @@ abstract class SongQueueSongMappingStore {
7272
return findHeadRaw(SimpleSQLiteQuery(query, args))
7373
}
7474

75+
protected abstract fun entriesByIdsRaw(query: SupportSQLiteQuery): Map<
76+
@MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping>
77+
78+
fun entriesByIds(queueId: String, songMappingIds: List<String>): Map<
79+
String, Song.AlongSongQueueMapping> {
80+
val query = "SELECT ${Song.TABLE}.*, " +
81+
"${SongQueueSongMapping.TABLE}.* " +
82+
"FROM ${SongQueueSongMapping.TABLE} " +
83+
"WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " +
84+
"AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_ID} " +
85+
"IN (${sqlqph(songMappingIds.size)}) " +
86+
"LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " +
87+
"ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC"
88+
val args = arrayOf(queueId, *songMappingIds.toTypedArray())
89+
return entriesByIdsRaw(SimpleSQLiteQuery(query, args))
90+
}
91+
92+
protected abstract fun entriesByNextIdsRaw(query: SupportSQLiteQuery): Map<
93+
@MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping>
94+
95+
fun entriesByNextIds(queueId: String, songMappingIds: List<String>): Map<
96+
String, Song.AlongSongQueueMapping> {
97+
val query = "SELECT ${Song.TABLE}.*, " +
98+
"${SongQueueSongMapping.TABLE}.* " +
99+
"FROM ${SongQueueSongMapping.TABLE} " +
100+
"WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " +
101+
"AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_NEXT_ID} " +
102+
"IN (${sqlqph(songMappingIds.size)}) " +
103+
"LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " +
104+
"ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC"
105+
val args = arrayOf(queueId, *songMappingIds.toTypedArray())
106+
return entriesByNextIdsRaw(SimpleSQLiteQuery(query, args))
107+
}
108+
75109
@RawQuery(observedEntities = [Song::class, SongQueueSongMapping::class])
76110
protected abstract fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow<
77-
Map<@MapColumn(SongQueueSongMapping.COLUMN_SONG_ID) String, Song.AlongSongQueueMapping>>
111+
Map<@MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping>>
78112

79113
fun entriesAsFlow(queueId: String): Flow<Map<String, Song.AlongSongQueueMapping>> {
80114
val query = "SELECT ${Song.TABLE}.*, " +

app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt

Lines changed: 71 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,54 @@
11
package io.github.zyrouge.symphony.services.radio
22

33
import io.github.zyrouge.symphony.Symphony
4+
import io.github.zyrouge.symphony.services.database.store.SongQueueSongMappingStore
45
import io.github.zyrouge.symphony.services.groove.entities.Song
56
import io.github.zyrouge.symphony.services.groove.entities.SongQueue
67
import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping
8+
import io.github.zyrouge.symphony.utils.complex_linked_list.ComplexLinkedListOperator
79

810
class RadioQueue(private val symphony: Symphony) {
11+
private class SongQueueSongMappingOperatorEntityFunctions :
12+
ComplexLinkedListOperator.EntityFunctions<String, SongQueueSongMapping> {
13+
override fun getEntityId(entity: SongQueueSongMapping) = entity.id
14+
override fun getEntityNextId(entity: SongQueueSongMapping) = entity.nextId
15+
override fun getEntityIsHead(entity: SongQueueSongMapping) = entity.isHead
16+
17+
override fun updateEntityNextId(entity: SongQueueSongMapping, nNextId: String?) =
18+
entity.copy(nextId = nNextId)
19+
20+
override fun updateEntityIsHead(entity: SongQueueSongMapping, nIsHead: Boolean) =
21+
entity.copy(isHead = nIsHead)
22+
}
23+
24+
private class SongQueueSongMappingOperatorPersistenceFunctions(
25+
private val store: SongQueueSongMappingStore,
26+
private val queueId: String,
27+
) :
28+
ComplexLinkedListOperator.PersistenceFunctions<String, SongQueueSongMapping> {
29+
override fun getEntitiesByIds(ids: List<String>) = store.entriesByIds(queueId, ids)
30+
.mapValues { it.value.mapping }
31+
32+
override fun getEntitiesByNextIds(nextIds: List<String>) =
33+
store.entriesByNextIds(queueId, nextIds)
34+
.mapValues { it.value.mapping }
35+
36+
override fun getHeadEntity() = store.findHead(queueId)?.mapping
37+
override fun getTailEntity() = store.findByNextId(queueId, null)?.mapping
38+
39+
override suspend fun insertEntities(entities: List<SongQueueSongMapping>) {
40+
store.insert(*entities.toTypedArray())
41+
}
42+
43+
override suspend fun updateEntities(entities: List<SongQueueSongMapping>) {
44+
store.update(*entities.toTypedArray())
45+
}
46+
47+
override suspend fun deleteEntities(ids: List<String>) {
48+
store.delete(queueId, ids)
49+
}
50+
}
51+
952
// val queueFlow = symphony.database.songQueue.findFirstAsFlow()
1053
// val queue = AtomicReference<SongQueue.AlongAttributes?>(null)
1154

@@ -54,50 +97,37 @@ class RadioQueue(private val symphony: Symphony) {
5497
)
5598
symphony.database.songQueue.insert(queue)
5699
}
57-
var previousSong = previousSongMappingId?.let {
58-
symphony.database.songQueueSongMapping.findById(queueId, it)
59-
}
60-
var nextMappingId = previousSong?.mapping?.nextId
61-
var ogNextMappingId = previousSong?.mapping?.ogNextId
62-
val added = mutableListOf<SongQueueSongMapping>()
63-
var i = 0
64-
val songIdsCount = songIds.size
65-
for (x in songIds.reversed()) {
66-
val isHead = origQueue == null && i == songIdsCount - 1
67-
val mapping = SongQueueSongMapping(
100+
val operator = createSongQueueSongMappingOperator(queueId)
101+
operator.add(previousSongMappingId, songIds) { x, isHead, nextId ->
102+
SongQueueSongMapping(
68103
id = symphony.database.songQueueSongMappingIdGenerator.next(),
69104
queueId = queueId,
70105
songId = x,
71106
isHead = isHead,
72-
nextId = nextMappingId,
73-
ogNextId = ogNextMappingId,
107+
nextId = nextId,
108+
ogNextId = nextId,
74109
)
75-
added.add(mapping)
76-
nextMappingId = mapping.id
77-
ogNextMappingId = mapping.id
78-
i++
79110
}
80-
symphony.database.songQueueSongMapping.insert(*added.toTypedArray())
81111
afterAdd(options)
82112
}
83113

84114
suspend fun add(
85115
songId: String,
86-
previousSongId: String? = null,
116+
previousSongMappingId: String? = null,
87117
options: Radio.PlayOptions = Radio.PlayOptions(),
88-
) = add(listOf(songId), previousSongId, options)
118+
) = add(listOf(songId), previousSongMappingId, options)
89119

90120
suspend fun add(
91121
songs: List<Song>,
92-
previousSongId: String? = null,
122+
previousSongMappingId: String? = null,
93123
options: Radio.PlayOptions = Radio.PlayOptions(),
94-
) = add(songs.map { it.id }, previousSongId, options)
124+
) = add(songs.map { it.id }, previousSongMappingId, options)
95125

96126
suspend fun add(
97127
song: Song,
98-
previousSongId: String? = null,
128+
previousSongMappingId: String? = null,
99129
options: Radio.PlayOptions = Radio.PlayOptions(),
100-
) = add(listOf(song.id), previousSongId, options)
130+
) = add(listOf(song.id), previousSongMappingId, options)
101131

102132
private fun afterAdd(options: Radio.PlayOptions) {
103133
if (!symphony.radio.hasPlayer) {
@@ -106,36 +136,18 @@ class RadioQueue(private val symphony: Symphony) {
106136
symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified)
107137
}
108138

109-
fun remove(id: String) {
110-
originalQueue.removeAt(index)
111-
currentQueue.removeAt(index)
112-
symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified)
113-
if (currentSongIndex == index) {
114-
symphony.radio.play(Radio.PlayOptions(index = currentSongIndex))
115-
} else if (index < currentSongIndex) {
116-
currentSongIndex--
139+
suspend fun remove(songMappingIds: List<String>): Boolean {
140+
val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT)
141+
if (queue == null) {
142+
return false
117143
}
144+
val queueId = queue.entity.id
145+
val operator = createSongQueueSongMappingOperator(queueId)
146+
val result = operator.remove(songMappingIds)
147+
return result.deletedKeys.isNotEmpty()
118148
}
119149

120-
fun remove(indices: List<Int>) {
121-
var deflection = 0
122-
var currentSongRemoved = false
123-
val sortedIndices = indices.sortedDescending()
124-
for (i in sortedIndices) {
125-
val index = i - deflection
126-
originalQueue.removeAt(index)
127-
currentQueue.removeAt(index)
128-
when {
129-
i < currentSongIndex -> deflection++
130-
i == currentSongIndex -> currentSongRemoved = true
131-
}
132-
}
133-
currentSongIndex -= deflection
134-
symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified)
135-
if (currentSongRemoved) {
136-
symphony.radio.play(Radio.PlayOptions(index = currentSongIndex))
137-
}
138-
}
150+
suspend fun remove(songMappingId: String) = remove(listOf(songMappingId))
139151

140152
fun setLoopMode(loopMode: LoopMode) {
141153
currentLoopMode = loopMode
@@ -169,6 +181,14 @@ class RadioQueue(private val symphony: Symphony) {
169181
symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified)
170182
}
171183

184+
private fun createSongQueueSongMappingOperator(queueId: String) = ComplexLinkedListOperator(
185+
SongQueueSongMappingOperatorEntityFunctions(),
186+
SongQueueSongMappingOperatorPersistenceFunctions(
187+
symphony.database.songQueueSongMapping,
188+
queueId
189+
)
190+
)
191+
172192
companion object {
173193
const val SONG_QUEUE_INTERNAL_ID_DEFAULT = 1
174194
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.github.zyrouge.symphony.utils.complex_linked_list
2+
3+
typealias ComplexLinkedListAdditionOperatorCreateFn<K, V, X> = (value: X, isHead: Boolean, nextId: K?) -> V
4+
5+
class ComplexLinkedListAdditionOperator<K, V, X>(
6+
val helper: ComplexLinkedListOperator<K, V>,
7+
val insertAtId: K?,
8+
val values: List<X>,
9+
val createFn: ComplexLinkedListAdditionOperatorCreateFn<K, V, X>,
10+
) {
11+
data class Result<K>(val addedKeys: List<K>)
12+
13+
suspend fun operate(): Result<K> {
14+
val headEntity = helper.persistenceFunctions.getHeadEntity()
15+
val tailEntity = insertAtId?.let { helper.persistenceFunctions.getEntity(it) }
16+
?: helper.persistenceFunctions.getTailEntity()
17+
val addedKeys = mutableListOf<K>()
18+
val added = mutableListOf<V>()
19+
var nextId = tailEntity?.let { helper.entityFunctions.getEntityNextId(it) }
20+
val count = values.size
21+
for (i in (count - 1) downTo 0) {
22+
val value = values[i]
23+
val isHead = i == 0 && headEntity == null
24+
val entity = createFn(value, isHead, nextId)
25+
val id = helper.entityFunctions.getEntityId(entity)
26+
addedKeys.add(id)
27+
added.add(entity)
28+
nextId = id
29+
}
30+
helper.persistenceFunctions.insertEntities(added)
31+
return Result(addedKeys = addedKeys)
32+
}
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.github.zyrouge.symphony.utils.complex_linked_list
2+
3+
class ComplexLinkedListOperator<K, V>(
4+
val entityFunctions: EntityFunctions<K, V>,
5+
val persistenceFunctions: PersistenceFunctions<K, V>,
6+
) {
7+
interface EntityFunctions<K, V> {
8+
fun getEntityId(entity: V): K
9+
fun getEntityNextId(entity: V): K?
10+
fun getEntityIsHead(entity: V): Boolean
11+
fun updateEntityNextId(entity: V, nNextId: K?): V
12+
fun updateEntityIsHead(entity: V, nIsHead: Boolean): V
13+
}
14+
15+
interface PersistenceFunctions<K, V> {
16+
fun getEntitiesByIds(ids: List<K>): Map<K, V>
17+
fun getEntity(id: K) = getEntitiesByIds(listOf(id))[id]
18+
fun getEntitiesByNextIds(nextIds: List<K>): Map<K, V>
19+
fun getEntityByNextId(nextId: K) = getEntitiesByIds(listOf(nextId))[nextId]
20+
fun getHeadEntity(): V?
21+
fun getTailEntity(): V?
22+
suspend fun insertEntities(entities: List<V>)
23+
suspend fun updateEntities(entities: List<V>)
24+
suspend fun deleteEntities(ids: List<K>)
25+
}
26+
27+
suspend fun <X> add(
28+
insertAtId: K?,
29+
values: List<X>,
30+
createFn: ComplexLinkedListAdditionOperatorCreateFn<K, V, X>,
31+
) = ComplexLinkedListAdditionOperator(this, insertAtId, values, createFn).operate()
32+
33+
suspend fun remove(keys: List<K>) = ComplexLinkedListRemoveOperator(this, keys).operate()
34+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.github.zyrouge.symphony.utils.complex_linked_list
2+
3+
class ComplexLinkedListRemoveOperator<K, V>(
4+
val helper: ComplexLinkedListOperator<K, V>,
5+
val keys: List<K>,
6+
) {
7+
data class Result<K>(
8+
val headChanged: Boolean,
9+
val modifiedKeys: List<K>,
10+
val deletedKeys: List<K>,
11+
)
12+
13+
suspend fun operate(): Result<K> {
14+
val entities = helper.persistenceFunctions.getEntitiesByIds(keys).toMutableMap()
15+
val idToPreviousId = mutableMapOf<K, K>()
16+
for (x in entities.values) {
17+
val id = helper.entityFunctions.getEntityId(x)
18+
val nextId = helper.entityFunctions.getEntityId(x)
19+
idToPreviousId[nextId] = id
20+
}
21+
for (x in helper.persistenceFunctions.getEntitiesByNextIds(keys).values) {
22+
val id = helper.entityFunctions.getEntityId(x)
23+
val nextId = helper.entityFunctions.getEntityId(x)
24+
entities.put(id, x)
25+
idToPreviousId.put(nextId, id)
26+
}
27+
val modified = mutableSetOf<K>()
28+
val deleted = mutableSetOf<K>()
29+
var headChanged = false
30+
for (id in keys) {
31+
val entity = entities[id] ?: continue
32+
val isHead = helper.entityFunctions.getEntityIsHead(entity)
33+
if (isHead) {
34+
val nextId = helper.entityFunctions.getEntityNextId(entity) ?: continue
35+
val nextEntity = entities[nextId] ?: continue
36+
val nNextEntity = helper.entityFunctions.updateEntityIsHead(nextEntity, true)
37+
entities.put(nextId, nNextEntity)
38+
entities.remove(id)
39+
modified.add(nextId)
40+
modified.remove(id)
41+
deleted.add(id)
42+
headChanged = true
43+
continue
44+
}
45+
val previousId = idToPreviousId[id] ?: continue
46+
val previousEntity = entities[previousId] ?: continue
47+
val nextId = helper.entityFunctions.getEntityNextId(entity)
48+
val nPreviousEntity = helper.entityFunctions.updateEntityNextId(previousEntity, nextId)
49+
entities.put(previousId, nPreviousEntity)
50+
entities.remove(id)
51+
modified.add(previousId)
52+
modified.remove(id)
53+
deleted.add(id)
54+
}
55+
if (modified.isNotEmpty()) {
56+
val modifiedEntities = modified.mapNotNull { entities[it] }.toList()
57+
helper.persistenceFunctions.updateEntities(modifiedEntities)
58+
}
59+
if (deleted.isNotEmpty()) {
60+
helper.persistenceFunctions.deleteEntities(deleted.toList())
61+
}
62+
return Result(
63+
headChanged = headChanged,
64+
modifiedKeys = modified.toList(),
65+
deletedKeys = deleted.toList(),
66+
)
67+
}
68+
}

0 commit comments

Comments
 (0)