music: back playlists with database
Finally persist playlists with a backing database.
This commit is contained in:
parent
0597fa876c
commit
c86970470f
7 changed files with 186 additions and 48 deletions
|
@ -299,8 +299,13 @@ constructor(
|
|||
*/
|
||||
fun savePlaylistEdit() {
|
||||
val playlist = _currentPlaylist.value ?: return
|
||||
val editedPlaylist = (_editedPlaylist.value ?: return).also { _editedPlaylist.value = null }
|
||||
musicRepository.rewritePlaylist(playlist, editedPlaylist)
|
||||
val editedPlaylist = _editedPlaylist.value ?: return
|
||||
viewModelScope.launch {
|
||||
musicRepository.rewritePlaylist(playlist, editedPlaylist)
|
||||
// TODO: The user could probably press some kind of button if they were fast enough.
|
||||
// Think of a better way to handle this state.
|
||||
_editedPlaylist.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -116,7 +116,7 @@ interface MusicRepository {
|
|||
* @param name The name of the new [Playlist].
|
||||
* @param songs The songs to populate the new [Playlist] with.
|
||||
*/
|
||||
fun createPlaylist(name: String, songs: List<Song>)
|
||||
suspend fun createPlaylist(name: String, songs: List<Song>)
|
||||
|
||||
/**
|
||||
* Rename a [Playlist].
|
||||
|
@ -124,14 +124,14 @@ interface MusicRepository {
|
|||
* @param playlist The [Playlist] to rename.
|
||||
* @param name The name of the new [Playlist].
|
||||
*/
|
||||
fun renamePlaylist(playlist: Playlist, name: String)
|
||||
suspend fun renamePlaylist(playlist: Playlist, name: String)
|
||||
|
||||
/**
|
||||
* Delete a [Playlist].
|
||||
*
|
||||
* @param playlist The playlist to delete.
|
||||
*/
|
||||
fun deletePlaylist(playlist: Playlist)
|
||||
suspend fun deletePlaylist(playlist: Playlist)
|
||||
|
||||
/**
|
||||
* Add the given [Song]s to a [Playlist].
|
||||
|
@ -139,7 +139,7 @@ interface MusicRepository {
|
|||
* @param songs The [Song]s to add to the [Playlist].
|
||||
* @param playlist The [Playlist] to add to.
|
||||
*/
|
||||
fun addToPlaylist(songs: List<Song>, playlist: Playlist)
|
||||
suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist)
|
||||
|
||||
/**
|
||||
* Update the [Song]s of a [Playlist].
|
||||
|
@ -147,7 +147,7 @@ interface MusicRepository {
|
|||
* @param playlist The [Playlist] to update.
|
||||
* @param songs The new [Song]s to be contained in the [Playlist].
|
||||
*/
|
||||
fun rewritePlaylist(playlist: Playlist, songs: List<Song>)
|
||||
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>)
|
||||
|
||||
/**
|
||||
* Request that a music loading operation is started by the current [IndexingWorker]. Does
|
||||
|
@ -276,7 +276,7 @@ constructor(
|
|||
(deviceLibrary?.run { findSong(uid) ?: findAlbum(uid) ?: findArtist(uid) ?: findGenre(uid) }
|
||||
?: userLibrary?.findPlaylist(uid))
|
||||
|
||||
override fun createPlaylist(name: String, songs: List<Song>) {
|
||||
override suspend fun createPlaylist(name: String, songs: List<Song>) {
|
||||
val userLibrary = userLibrary ?: return
|
||||
userLibrary.createPlaylist(name, songs)
|
||||
for (listener in updateListeners) {
|
||||
|
@ -285,7 +285,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
val userLibrary = userLibrary ?: return
|
||||
userLibrary.renamePlaylist(playlist, name)
|
||||
for (listener in updateListeners) {
|
||||
|
@ -294,7 +294,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun deletePlaylist(playlist: Playlist) {
|
||||
override suspend fun deletePlaylist(playlist: Playlist) {
|
||||
val userLibrary = userLibrary ?: return
|
||||
userLibrary.deletePlaylist(playlist)
|
||||
for (listener in updateListeners) {
|
||||
|
@ -303,7 +303,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
|
||||
override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
|
||||
val userLibrary = userLibrary ?: return
|
||||
userLibrary.addToPlaylist(playlist, songs)
|
||||
for (listener in updateListeners) {
|
||||
|
@ -312,7 +312,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
val userLibrary = userLibrary ?: return
|
||||
userLibrary.rewritePlaylist(playlist, songs)
|
||||
for (listener in updateListeners) {
|
||||
|
|
|
@ -19,10 +19,13 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.util.Event
|
||||
import org.oxycblt.auxio.util.MutableEvent
|
||||
|
||||
|
@ -110,7 +113,7 @@ constructor(
|
|||
*/
|
||||
fun createPlaylist(name: String? = null, songs: List<Song> = listOf()) {
|
||||
if (name != null) {
|
||||
musicRepository.createPlaylist(name, songs)
|
||||
viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) }
|
||||
} else {
|
||||
_newPlaylistSongs.put(songs)
|
||||
}
|
||||
|
@ -124,7 +127,7 @@ constructor(
|
|||
*/
|
||||
fun renamePlaylist(playlist: Playlist, name: String? = null) {
|
||||
if (name != null) {
|
||||
musicRepository.renamePlaylist(playlist, name)
|
||||
viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) }
|
||||
} else {
|
||||
_playlistToRename.put(playlist)
|
||||
}
|
||||
|
@ -139,7 +142,7 @@ constructor(
|
|||
*/
|
||||
fun deletePlaylist(playlist: Playlist, rude: Boolean = false) {
|
||||
if (rude) {
|
||||
musicRepository.deletePlaylist(playlist)
|
||||
viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) }
|
||||
} else {
|
||||
_playlistToDelete.put(playlist)
|
||||
}
|
||||
|
@ -193,7 +196,7 @@ constructor(
|
|||
*/
|
||||
fun addToPlaylist(songs: List<Song>, playlist: Playlist? = null) {
|
||||
if (playlist != null) {
|
||||
musicRepository.addToPlaylist(songs, playlist)
|
||||
viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) }
|
||||
} else {
|
||||
_songsToAdd.put(songs)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ package org.oxycblt.auxio.music.user
|
|||
import androidx.room.*
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
||||
/**
|
||||
* Raw playlist information persisted to [UserMusicDatabase].
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
data class RawPlaylist(
|
||||
@Embedded val playlistInfo: PlaylistInfo,
|
||||
@Relation(
|
||||
|
@ -30,12 +34,26 @@ data class RawPlaylist(
|
|||
val songs: List<PlaylistSong>
|
||||
)
|
||||
|
||||
/**
|
||||
* UID and name information corresponding to a [RawPlaylist] entry.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@Entity data class PlaylistInfo(@PrimaryKey val playlistUid: Music.UID, val name: String)
|
||||
|
||||
/**
|
||||
* Song information corresponding to a [RawPlaylist] entry.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@Entity data class PlaylistSong(@PrimaryKey val songUid: Music.UID)
|
||||
|
||||
@Entity(primaryKeys = ["playlistUid", "songUid"])
|
||||
|
||||
/**
|
||||
* Links individual songs to a playlist entry.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@Entity
|
||||
data class PlaylistSongCrossRef(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val playlistUid: Music.UID,
|
||||
@ColumnInfo(index = true) val songUid: Music.UID
|
||||
val songUid: Music.UID
|
||||
)
|
||||
|
|
|
@ -57,11 +57,12 @@ interface UserLibrary {
|
|||
/**
|
||||
* Create a new [UserLibrary].
|
||||
*
|
||||
* @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later.
|
||||
* This allows database information to be read before the actual instance is constructed.
|
||||
* @param deviceLibraryChannel Asynchronously populated [DeviceLibrary] that can be obtained
|
||||
* later. This allows database information to be read before the actual instance is
|
||||
* constructed.
|
||||
* @return A new [MutableUserLibrary] with the required implementation.
|
||||
*/
|
||||
suspend fun read(deviceLibrary: Channel<DeviceLibrary>): MutableUserLibrary
|
||||
suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,7 +79,7 @@ interface MutableUserLibrary : UserLibrary {
|
|||
* @param name The name of the [Playlist].
|
||||
* @param songs The songs to place in the [Playlist].
|
||||
*/
|
||||
fun createPlaylist(name: String, songs: List<Song>)
|
||||
suspend fun createPlaylist(name: String, songs: List<Song>)
|
||||
|
||||
/**
|
||||
* Rename a [Playlist].
|
||||
|
@ -86,21 +87,21 @@ interface MutableUserLibrary : UserLibrary {
|
|||
* @param playlist The [Playlist] to rename.
|
||||
* @param name The name of the new [Playlist].
|
||||
*/
|
||||
fun renamePlaylist(playlist: Playlist, name: String)
|
||||
suspend fun renamePlaylist(playlist: Playlist, name: String)
|
||||
|
||||
/**
|
||||
* Delete a [Playlist].
|
||||
*
|
||||
* @param playlist The playlist to delete.
|
||||
*/
|
||||
fun deletePlaylist(playlist: Playlist)
|
||||
suspend fun deletePlaylist(playlist: Playlist)
|
||||
|
||||
/**
|
||||
* Add [Song]s to a [Playlist].
|
||||
*
|
||||
* @param playlist The [Playlist] to add to. Must currently exist.
|
||||
*/
|
||||
fun addToPlaylist(playlist: Playlist, songs: List<Song>)
|
||||
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>)
|
||||
|
||||
/**
|
||||
* Update the [Song]s of a [Playlist].
|
||||
|
@ -108,23 +109,32 @@ interface MutableUserLibrary : UserLibrary {
|
|||
* @param playlist The [Playlist] to update.
|
||||
* @param songs The new [Song]s to be contained in the [Playlist].
|
||||
*/
|
||||
fun rewritePlaylist(playlist: Playlist, songs: List<Song>)
|
||||
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>)
|
||||
}
|
||||
|
||||
class UserLibraryFactoryImpl
|
||||
@Inject
|
||||
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
||||
UserLibrary.Factory {
|
||||
override suspend fun read(deviceLibrary: Channel<DeviceLibrary>): MutableUserLibrary =
|
||||
UserLibraryImpl(playlistDao, deviceLibrary.receive(), musicSettings)
|
||||
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary {
|
||||
// While were waiting for the library, read our playlists out.
|
||||
val rawPlaylists = playlistDao.readRawPlaylists()
|
||||
val deviceLibrary = deviceLibraryChannel.receive()
|
||||
// Convert the database playlist information to actual usable playlists.
|
||||
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
||||
for (rawPlaylist in rawPlaylists) {
|
||||
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, musicSettings)
|
||||
playlistMap[playlistImpl.uid] = playlistImpl
|
||||
}
|
||||
return UserLibraryImpl(playlistDao, playlistMap, musicSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private class UserLibraryImpl(
|
||||
private val playlistDao: PlaylistDao,
|
||||
private val deviceLibrary: DeviceLibrary,
|
||||
private val playlistMap: MutableMap<Music.UID, PlaylistImpl>,
|
||||
private val musicSettings: MusicSettings
|
||||
) : MutableUserLibrary {
|
||||
private val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
||||
override val playlists: List<Playlist>
|
||||
get() = playlistMap.values.toList()
|
||||
|
||||
|
@ -132,35 +142,41 @@ private class UserLibraryImpl(
|
|||
|
||||
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
|
||||
|
||||
@Synchronized
|
||||
override fun createPlaylist(name: String, songs: List<Song>) {
|
||||
override suspend fun createPlaylist(name: String, songs: List<Song>) {
|
||||
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
|
||||
playlistMap[playlistImpl.uid] = playlistImpl
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
val rawPlaylist =
|
||||
RawPlaylist(
|
||||
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
|
||||
playlistImpl.songs.map { PlaylistSong(it.uid) })
|
||||
playlistDao.insertPlaylist(rawPlaylist)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
|
||||
playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings)
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) }
|
||||
playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun deletePlaylist(playlist: Playlist) {
|
||||
requireNotNull(playlistMap.remove(playlist.uid)) { "Cannot remove invalid playlist" }
|
||||
override suspend fun deletePlaylist(playlist: Playlist) {
|
||||
synchronized(this) {
|
||||
requireNotNull(playlistMap.remove(playlist.uid)) { "Cannot remove invalid playlist" }
|
||||
}
|
||||
playlistDao.deletePlaylist(playlist.uid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun addToPlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
|
||||
playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) }
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } }
|
||||
playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
|
||||
playlistMap[playlist.uid] = playlistImpl.edit(songs)
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) }
|
||||
playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import dagger.hilt.components.SingletonComponent
|
|||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UserModule {
|
||||
@Binds fun userLibaryFactory(factory: UserLibraryFactoryImpl): UserLibrary.Factory
|
||||
@Binds fun userLibraryFactory(factory: UserLibraryFactoryImpl): UserLibrary.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -21,16 +21,112 @@ package org.oxycblt.auxio.music.user
|
|||
import androidx.room.*
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
||||
/**
|
||||
* Allows persistence of all user-created music information.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@Database(
|
||||
entities = [PlaylistInfo::class, PlaylistSong::class, PlaylistSongCrossRef::class],
|
||||
version = 28,
|
||||
version = 30,
|
||||
exportSchema = false)
|
||||
@TypeConverters(Music.UID.TypeConverters::class)
|
||||
abstract class UserMusicDatabase : RoomDatabase() {
|
||||
abstract fun playlistDao(): PlaylistDao
|
||||
}
|
||||
|
||||
// TODO: Handle playlist defragmentation? I really don't want dead songs to accumulate in this
|
||||
// database.
|
||||
|
||||
/**
|
||||
* The DAO for persisted playlist information.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@Dao
|
||||
interface PlaylistDao {
|
||||
@Transaction @Query("SELECT * FROM PlaylistInfo") fun readRawPlaylists(): List<RawPlaylist>
|
||||
/**
|
||||
* Read out all playlists stored in the database.
|
||||
* @return A list of [RawPlaylist] representing each playlist stored.
|
||||
*/
|
||||
@Transaction
|
||||
@Query("SELECT * FROM PlaylistInfo")
|
||||
suspend fun readRawPlaylists(): List<RawPlaylist>
|
||||
|
||||
/**
|
||||
* Create a new playlist.
|
||||
* @param rawPlaylist The [RawPlaylist] to create.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun insertPlaylist(rawPlaylist: RawPlaylist) {
|
||||
insertInfo(rawPlaylist.playlistInfo)
|
||||
insertSongs(rawPlaylist.songs)
|
||||
insertRefs(
|
||||
rawPlaylist.songs.map {
|
||||
PlaylistSongCrossRef(
|
||||
playlistUid = rawPlaylist.playlistInfo.playlistUid, songUid = it.songUid)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the currently-stored [PlaylistInfo] for a playlist entry.
|
||||
* @param playlistInfo The new [PlaylistInfo] to store.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) {
|
||||
deleteInfo(playlistInfo.playlistUid)
|
||||
insertInfo(playlistInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a playlist entry's [PlaylistInfo] and [PlaylistSong].
|
||||
* @param playlistUid The [Music.UID] of the playlist to delete.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun deletePlaylist(playlistUid: Music.UID) {
|
||||
deleteInfo(playlistUid)
|
||||
deleteRefs(playlistUid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new song entries into a playlist.
|
||||
* @param playlistUid The [Music.UID] of the playlist to insert into.
|
||||
* @param songs The [PlaylistSong] representing each song to put into the playlist.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
||||
insertSongs(songs)
|
||||
insertRefs(
|
||||
songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the currently-stored [Song]s of the current playlist entry.
|
||||
* @param playlistUid The [Music.UID] of the playlist to update.
|
||||
* @param songs The [PlaylistSong] representing the new list of songs to be placed in the playlist.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
||||
deleteRefs(playlistUid)
|
||||
insertSongs(songs)
|
||||
insertRefs(
|
||||
songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) })
|
||||
}
|
||||
|
||||
/** Internal, do not use. */
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertInfo(info: PlaylistInfo)
|
||||
|
||||
/** Internal, do not use. */
|
||||
@Query("DELETE FROM PlaylistInfo where playlistUid = :playlistUid")
|
||||
suspend fun deleteInfo(playlistUid: Music.UID)
|
||||
|
||||
/** Internal, do not use. */
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertSongs(songs: List<PlaylistSong>)
|
||||
|
||||
/** Internal, do not use. */
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun insertRefs(refs: List<PlaylistSongCrossRef>)
|
||||
|
||||
/** Internal, do not use. */
|
||||
@Query("DELETE FROM PlaylistSongCrossRef where playlistUid = :playlistUid")
|
||||
suspend fun deleteRefs(playlistUid: Music.UID)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue