musikr: re-implement playlist loading

This commit is contained in:
Alexander Capehart 2024-12-16 18:45:51 -05:00
parent 50bfe9926b
commit 6e3b03d4c6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 91 additions and 27 deletions

View file

@ -39,6 +39,8 @@ import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheDatabase import org.oxycblt.musikr.cache.CacheDatabase
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.playlist.db.PlaylistDatabase
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
import timber.log.Timber as L import timber.log.Timber as L
@ -213,6 +215,7 @@ constructor(
private val musikr: Musikr, private val musikr: Musikr,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val cacheDatabase: CacheDatabase, private val cacheDatabase: CacheDatabase,
private val playlistDatabase: PlaylistDatabase,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
@ -361,10 +364,10 @@ constructor(
val storage = val storage =
if (withCache) { if (withCache) {
Storage(Cache.full(cacheDatabase), StoredCovers.from(context, "covers")) Storage(Cache.full(cacheDatabase), StoredCovers.from(context, "covers"), StoredPlaylists.from(playlistDatabase))
} else { } else {
// TODO: Revisioned covers // TODO: Revisioned cache (as a stateful extension of musikr)
Storage(Cache.writeOnly(cacheDatabase), StoredCovers.from(context, "covers")) Storage(Cache.writeOnly(cacheDatabase), StoredCovers.from(context, "covers"), StoredPlaylists.from(playlistDatabase))
} }
val newLibrary = val newLibrary =
musikr.run( musikr.run(

View file

@ -20,9 +20,10 @@ package org.oxycblt.musikr
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
data class Storage(val cache: Cache, val storedCovers: StoredCovers) data class Storage(val cache: Cache, val storedCovers: StoredCovers, val storedPlaylists: StoredPlaylists)
data class Interpretation(val naming: Naming, val separators: Separators) data class Interpretation(val naming: Naming, val separators: Separators)

View file

@ -70,7 +70,7 @@ private class MusikrImpl(
var extractedCount = 0 var extractedCount = 0
val explored = val explored =
exploreStep exploreStep
.explore(locations) .explore(locations, storage)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
.onStart { onProgress(IndexingProgress.Songs(0, 0)) } .onStart { onProgress(IndexingProgress.Songs(0, 0)) }
.onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) } .onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) }

View file

@ -15,42 +15,64 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.musikr.pipeline package org.oxycblt.musikr.pipeline
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.query.DeviceFiles import org.oxycblt.musikr.fs.query.DeviceFiles
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U import org.oxycblt.musikr.playlist.m3u.M3U
internal interface ExploreStep { internal interface ExploreStep {
fun explore(locations: List<MusicLocation>): Flow<ExploreNode> fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode>
companion object { companion object {
fun from(context: Context): ExploreStep = ExploreStepImpl(DeviceFiles.from(context)) fun from(context: Context): ExploreStep =
ExploreStepImpl(DeviceFiles.from(context))
} }
} }
private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreStep { private class ExploreStepImpl(
override fun explore(locations: List<MusicLocation>) = private val deviceFiles: DeviceFiles
deviceFiles ) : ExploreStep {
.explore(locations.asFlow()) override fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode> {
.mapNotNull { val audios =
when { deviceFiles
it.mimeType == M3U.MIME_TYPE -> null .explore(locations.asFlow())
it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it) .mapNotNull {
else -> null when {
it.mimeType == M3U.MIME_TYPE -> null
it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it)
else -> null
}
} }
} .flowOn(Dispatchers.IO)
.flowOn(Dispatchers.IO) .buffer()
val playlists =
flow { emitAll(storage.storedPlaylists.read().asFlow()) }
.map { ExploreNode.Playlist(it) }
.flowOn(Dispatchers.IO)
.buffer()
return merge(audios, playlists)
}
} }
internal sealed interface ExploreNode { internal sealed interface ExploreNode {
data class Audio(val file: DeviceFile) : ExploreNode data class Audio(val file: DeviceFile) : ExploreNode
data class Playlist(val file: PlaylistFile) : ExploreNode
} }

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.Song
data class PlaylistFile( data class PlaylistFile(
val name: String, val name: String,
val songPointers: List<SongPointer>, val songPointers: List<SongPointer>,
val editor: PlaylistHandle val handle: PlaylistHandle
) )
sealed interface SongPointer { sealed interface SongPointer {

View file

@ -0,0 +1,36 @@
package org.oxycblt.musikr.playlist.db
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.playlist.PlaylistHandle
import java.util.UUID
internal class StoredPlaylistHandle(
private val playlistInfo: PlaylistInfo,
private val playlistDao: PlaylistDao
) : PlaylistHandle {
override val uid = playlistInfo.playlistUid
override suspend fun rename(name: String) {
playlistDao.replacePlaylistInfo(playlistInfo.copy(name = name))
}
override suspend fun rewrite(songs: List<Song>) {
playlistDao.replacePlaylistSongs(
uid,
songs.map { PlaylistSong(it.uid) }
)
}
override suspend fun add(songs: List<Song>) {
playlistDao.insertPlaylistSongs(
uid,
songs.map { PlaylistSong(it.uid) }
)
}
override suspend fun delete() {
playlistDao.deletePlaylist(uid)
}
}

View file

@ -18,15 +18,11 @@
package org.oxycblt.musikr.playlist.db package org.oxycblt.musikr.playlist.db
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.SongPointer
interface StoredPlaylists { interface StoredPlaylists {
fun read(): Flow<PlaylistFile> suspend fun read(): List<PlaylistFile>
companion object { companion object {
fun from(database: PlaylistDatabase): StoredPlaylists = fun from(database: PlaylistDatabase): StoredPlaylists =
@ -35,5 +31,11 @@ interface StoredPlaylists {
} }
private class StoredPlaylistsImpl(private val playlistDao: PlaylistDao) : StoredPlaylists { private class StoredPlaylistsImpl(private val playlistDao: PlaylistDao) : StoredPlaylists {
override fun read() = flow { emitAll(playlistDao.readRawPlaylists().asFlow().map { TODO() }) } override suspend fun read() = playlistDao.readRawPlaylists().map {
PlaylistFile(
it.playlistInfo.name,
it.songs.map { song -> SongPointer.UID(song.songUid) },
StoredPlaylistHandle(it.playlistInfo, playlistDao)
)
}
} }