From 88bce610ca99c3b1213eb6d5293ac6cd4286a97e Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 20 Dec 2023 11:03:32 -0700 Subject: [PATCH] music: connect playlist importing to frontend --- .../org/oxycblt/auxio/music/MusicViewModel.kt | 26 +++++++++++++++++++ .../auxio/music/device/DeviceLibrary.kt | 12 +++++++++ .../java/org/oxycblt/auxio/music/fs/Fs.kt | 6 ++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 314b38785..5d7568c71 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.music +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.oxycblt.auxio.list.ListSettings +import org.oxycblt.auxio.music.import.PlaylistImporter import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD @@ -42,6 +44,7 @@ class MusicViewModel constructor( private val listSettings: ListSettings, private val musicRepository: MusicRepository, + private val playlistImporter: PlaylistImporter ) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener { private val _indexingState = MutableStateFlow(null) @@ -61,6 +64,10 @@ constructor( val playlistDecision: Event get() = _playlistDecision + private val _importError = MutableEvent() + /** Flag for when playlist importing failed. Consume this and show an error if active. */ + val importError: Event get() = _importError + init { musicRepository.addUpdateListener(this) musicRepository.addIndexingListener(this) @@ -116,6 +123,25 @@ constructor( } } + /** + * Import a playlist from a file [Uri]. Errors pushed to [importError]. + * @param uri The [Uri] of the file to import. + * @see PlaylistImporter + */ + fun importPlaylist(uri: Uri) = + viewModelScope.launch(Dispatchers.IO) { + val importedPlaylist = playlistImporter.import(uri) + if (importedPlaylist == null) { + _importError.put(Unit) + return@launch + } + + val deviceLibrary = musicRepository.deviceLibrary ?: return@launch + val songs = importedPlaylist.paths.mapNotNull(deviceLibrary::findSongByPath) + + createPlaylist(importedPlaylist.name, songs) + } + /** * Rename the given playlist. * diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 389b34fb6..09ac80aef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.music.info.Name @@ -74,6 +75,14 @@ interface DeviceLibrary { */ fun findSongForUri(context: Context, uri: Uri): Song? + /** + * Find a [Song] instance corresponding to the given [Path]. + * + * @param path [Path] to search for. + * @return A [Song] corresponding to the given [Path], or null if one could not be found. + */ + fun findSongByPath(path: Path): Song? + /** * Find a [Album] instance corresponding to the given [Music.UID]. * @@ -266,6 +275,7 @@ class DeviceLibraryImpl( ) : DeviceLibrary { // Use a mapping to make finding information based on it's UID much faster. private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } + private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } @@ -287,6 +297,8 @@ class DeviceLibraryImpl( override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] + override fun findSongByPath(path: Path) = songPathMap[path] + override fun findSongForUri(context: Context, uri: Uri) = context.contentResolverSafe.useQuery( uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 494141fb7..1f152ed7e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -46,8 +46,6 @@ data class Path( val directory: Path get() = Path(volume, components.parent()) - override fun toString() = "Path(storageVolume=$volume, components=$components)" - /** * Transforms this [Path] into a "file" of the given name that's within the "directory" * represented by the current path. Ex. "/storage/emulated/0/Music" -> @@ -169,7 +167,7 @@ class VolumeManagerImpl @Inject constructor(private val storageManager: StorageM } } - private class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal { + private data class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal { override val mediaStoreName get() = storageVolume.mediaStoreVolumeNameCompat @@ -179,7 +177,7 @@ class VolumeManagerImpl @Inject constructor(private val storageManager: StorageM override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) } - private class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External { + private data class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External { override val id get() = storageVolume.uuidCompat