diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 050b5de16..f2527a752 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -159,8 +159,8 @@ constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.library) return - val library = musicRepository.library ?: return + if (!changes.deviceLibrary) return + val deviceLibrary = musicRepository.deviceLibrary ?: return // If we are showing any item right now, we will need to refresh it (and any information // related to it) with the new library in order to prevent stale items from showing up @@ -168,25 +168,25 @@ constructor( val song = currentSong.value if (song != null) { - _currentSong.value = library.sanitize(song)?.also(::refreshAudioInfo) + _currentSong.value = deviceLibrary.findSong(song.uid)?.also(::refreshAudioInfo) logD("Updated song to ${currentSong.value}") } val album = currentAlbum.value if (album != null) { - _currentAlbum.value = library.sanitize(album)?.also(::refreshAlbumList) + _currentAlbum.value = deviceLibrary.findAlbum(album.uid)?.also(::refreshAlbumList) logD("Updated genre to ${currentAlbum.value}") } val artist = currentArtist.value if (artist != null) { - _currentArtist.value = library.sanitize(artist)?.also(::refreshArtistList) + _currentArtist.value = deviceLibrary.findArtist(artist.uid)?.also(::refreshArtistList) logD("Updated genre to ${currentArtist.value}") } val genre = currentGenre.value if (genre != null) { - _currentGenre.value = library.sanitize(genre)?.also(::refreshGenreList) + _currentGenre.value = deviceLibrary.findGenre(genre.uid)?.also(::refreshGenreList) logD("Updated genre to ${currentGenre.value}") } } @@ -203,7 +203,7 @@ constructor( return } logD("Opening Song [uid: $uid]") - _currentSong.value = requireMusic(uid)?.also(::refreshAudioInfo) + _currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) } /** @@ -218,7 +218,8 @@ constructor( return } logD("Opening Album [uid: $uid]") - _currentAlbum.value = requireMusic(uid)?.also(::refreshAlbumList) + _currentAlbum.value = + musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList) } /** @@ -233,7 +234,8 @@ constructor( return } logD("Opening Artist [uid: $uid]") - _currentArtist.value = requireMusic(uid)?.also(::refreshArtistList) + _currentArtist.value = + musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList) } /** @@ -248,11 +250,10 @@ constructor( return } logD("Opening Genre [uid: $uid]") - _currentGenre.value = requireMusic(uid)?.also(::refreshGenreList) + _currentGenre.value = + musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList) } - private fun requireMusic(uid: Music.UID) = musicRepository.library?.find(uid) - private fun refreshAudioInfo(song: Song) { // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index d280a1e49..f15ca89ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -228,8 +228,7 @@ class GenreDetailFragment : is Genre -> { navModel.exploreNavigationItem.consume() } - is Playlist -> TODO("handle this") - null -> {} + else -> {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index d4a6c1c9c..bc2baefe9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -136,33 +136,33 @@ constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - val library = musicRepository.library - if (changes.library && library != null) { + val deviceLibrary = musicRepository.deviceLibrary + if (changes.deviceLibrary && deviceLibrary != null) { logD("Refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. _songsInstructions.put(UpdateInstructions.Diff) - _songsList.value = musicSettings.songSort.songs(library.songs) + _songsList.value = musicSettings.songSort.songs(deviceLibrary.songs) _albumsInstructions.put(UpdateInstructions.Diff) - _albumsLists.value = musicSettings.albumSort.albums(library.albums) + _albumsLists.value = musicSettings.albumSort.albums(deviceLibrary.albums) _artistsInstructions.put(UpdateInstructions.Diff) _artistsList.value = musicSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { // Hide Collaborators is enabled, filter out collaborators. - library.artists.filter { !it.isCollaborator } + deviceLibrary.artists.filter { !it.isCollaborator } } else { - library.artists + deviceLibrary.artists }) _genresInstructions.put(UpdateInstructions.Diff) - _genresList.value = musicSettings.genreSort.genres(library.genres) + _genresList.value = musicSettings.genreSort.genres(deviceLibrary.genres) } - val playlists = musicRepository.playlists - if (changes.playlists && playlists != null) { + val userLibrary = musicRepository.userLibrary + if (changes.userLibrary && userLibrary != null) { logD("Refreshing playlists") _playlistsInstructions.put(UpdateInstructions.Diff) - _playlistsList.value = musicSettings.playlistSort.playlists(playlists) + _playlistsList.value = musicSettings.playlistSort.playlists(userLibrary.playlists) } } @@ -175,7 +175,7 @@ constructor( override fun onHideCollaboratorsChanged() { // Changes in the hide collaborator setting will change the artist contents // of the library, consider it a library update. - onMusicChanges(MusicRepository.Changes(library = true, playlists = false)) + onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 6104ebee1..42fea7d41 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -43,18 +43,19 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.library) return - val library = musicRepository.library ?: return + if (!changes.deviceLibrary) return + val deviceLibrary = musicRepository.deviceLibrary ?: return + val userLibrary = musicRepository.userLibrary ?: return // Sanitize the selection to remove items that no longer exist and thus // won't appear in any list. _selected.value = _selected.value.mapNotNull { when (it) { - is Song -> library.sanitize(it) - is Album -> library.sanitize(it) - is Artist -> library.sanitize(it) - is Genre -> library.sanitize(it) - is Playlist -> TODO("handle this") + is Song -> deviceLibrary.findSong(it.uid) + is Album -> deviceLibrary.findAlbum(it.uid) + is Artist -> deviceLibrary.findArtist(it.uid) + is Genre -> deviceLibrary.findGenre(it.uid) + is Playlist -> userLibrary.findPlaylist(it.uid) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index f0f32a8b2..a764ef8ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -30,11 +30,11 @@ import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.music.fs.MimeType +import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.metadata.Date import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.ReleaseType -import org.oxycblt.auxio.music.storage.MimeType -import org.oxycblt.auxio.music.storage.Path import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull @@ -139,10 +139,10 @@ sealed interface Music : Item { object TypeConverters { /** @see [Music.UID.toString] */ - @TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString() + @TypeConverter fun fromMusicUID(uid: UID?) = uid?.toString() /** @see [Music.UID.fromString] */ - @TypeConverter fun toMusicUid(string: String?) = string?.let(Music.UID::fromString) + @TypeConverter fun toMusicUid(string: String?) = string?.let(UID::fromString) } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index e6517d781..6295dda5b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -26,10 +26,11 @@ import javax.inject.Inject import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import org.oxycblt.auxio.music.cache.CacheRepository -import org.oxycblt.auxio.music.library.Library -import org.oxycblt.auxio.music.library.RawSong +import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.fs.MediaStoreExtractor import org.oxycblt.auxio.music.metadata.TagExtractor -import org.oxycblt.auxio.music.storage.MediaStoreExtractor +import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW @@ -43,10 +44,10 @@ import org.oxycblt.auxio.util.logW * @author Alexander Capehart (OxygenCobalt) */ interface MusicRepository { - /** The current immutable music library loaded from the file-system. */ - val library: Library? - /** The current mutable user-defined playlists loaded from the file-system. */ - val playlists: List? + /** The current music information found on the device. */ + val deviceLibrary: DeviceLibrary? + /** The current user-defined music information. */ + val userLibrary: UserLibrary? /** The current state of music loading. Null if no load has occurred yet. */ val indexingState: IndexingState? @@ -96,6 +97,16 @@ interface MusicRepository { */ fun unregisterWorker(worker: IndexingWorker) + /** + * Generically search for the [Music] associated with the given [Music.UID]. Note that this + * method is much slower that type-specific find implementations, so this should only be used if + * the type of music being searched for is entirely unknown. + * + * @param uid The [Music.UID] to search for. + * @return The expected [Music] information, or null if it could not be found. + */ + fun find(uid: Music.UID): Music? + /** * Request that a music loading operation is started by the current [IndexingWorker]. Does * nothing if one is not available. @@ -118,7 +129,7 @@ interface MusicRepository { /** * Called when a change to the stored music information occurs. * - * @param changes The [Changes] that have occured. + * @param changes The [Changes] that have occurred. */ fun onMusicChanges(changes: Changes) } @@ -126,10 +137,10 @@ interface MusicRepository { /** * Flags indicating which kinds of music information changed. * - * @param library Whether the current [Library] has changed. - * @param playlists Whether the current [Playlist]s have changed. + * @param deviceLibrary Whether the current [DeviceLibrary] has changed. + * @param userLibrary Whether the current [Playlist]s have changed. */ - data class Changes(val library: Boolean, val playlists: Boolean) + data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean) /** A listener for events in the music loading process. */ interface IndexingListener { @@ -158,17 +169,18 @@ interface MusicRepository { class MusicRepositoryImpl @Inject constructor( - private val musicSettings: MusicSettings, private val cacheRepository: CacheRepository, private val mediaStoreExtractor: MediaStoreExtractor, - private val tagExtractor: TagExtractor + private val tagExtractor: TagExtractor, + private val deviceLibraryProvider: DeviceLibrary.Provider, + private val userLibraryProvider: UserLibrary.Provider ) : MusicRepository { private val updateListeners = mutableListOf() private val indexingListeners = mutableListOf() private var indexingWorker: MusicRepository.IndexingWorker? = null - override var library: Library? = null - override var playlists: List? = null + override var deviceLibrary: DeviceLibrary? = null + override var userLibrary: UserLibrary? = null private var previousCompletedState: IndexingState.Completed? = null private var currentIndexingState: IndexingState? = null override val indexingState: IndexingState? @@ -216,6 +228,10 @@ constructor( currentIndexingState = null } + override fun find(uid: Music.UID) = + (deviceLibrary?.run { findSong(uid) ?: findAlbum(uid) ?: findArtist(uid) ?: findGenre(uid) } + ?: userLibrary?.findPlaylist(uid)) + override fun requestIndex(withCache: Boolean) { indexingWorker?.requestIndex(withCache) } @@ -295,16 +311,20 @@ constructor( // parallel. logD("Discovered ${rawSongs.size} songs, starting finalization") emitLoading(IndexingProgress.Indeterminate) - val libraryJob = - worker.scope.async(Dispatchers.Main) { Library.from(rawSongs, musicSettings) } + val deviceLibraryChannel = Channel() + val deviceLibraryJob = + worker.scope.async(Dispatchers.Main) { + deviceLibraryProvider.create(rawSongs).also { deviceLibraryChannel.send(it) } + } + val userLibraryJob = worker.scope.async { userLibraryProvider.read(deviceLibraryChannel) } if (cache == null || cache.invalidated) { cacheRepository.writeCache(rawSongs) } - val newLibrary = libraryJob.await() - // TODO: Make real playlist reading + val deviceLibrary = deviceLibraryJob.await() + val userLibrary = userLibraryJob.await() withContext(Dispatchers.Main) { emitComplete(null) - emitData(newLibrary, listOf()) + emitData(deviceLibrary, userLibrary) } } @@ -330,14 +350,14 @@ constructor( } @Synchronized - private fun emitData(library: Library, playlists: List) { - val libraryChanged = this.library != library - val playlistsChanged = this.playlists != playlists - if (!libraryChanged && !playlistsChanged) return + private fun emitData(deviceLibrary: DeviceLibrary, userLibrary: UserLibrary) { + val deviceLibraryChanged = this.deviceLibrary != deviceLibrary + val userLibraryChanged = this.userLibrary != userLibrary + if (!deviceLibraryChanged && !userLibraryChanged) return - this.library = library - this.playlists = playlists - val changes = MusicRepository.Changes(libraryChanged, playlistsChanged) + this.deviceLibrary = deviceLibrary + this.userLibrary = userLibrary + val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged) for (listener in updateListeners) { listener.onMusicChanges(changes) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 6d6af6d46..94d7e9968 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -25,8 +25,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.storage.Directory -import org.oxycblt.auxio.music.storage.MusicDirectories +import org.oxycblt.auxio.music.fs.Directory +import org.oxycblt.auxio.music.fs.MusicDirectories import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getSystemServiceCompat @@ -62,7 +62,7 @@ interface MusicSettings : Settings { var artistSongSort: Sort /** The [Sort] mode used in an [Genre]'s [Song] list. */ var genreSongSort: Sort - + /** The [] */ interface Listener { /** Called when a setting controlling how music is loaded has changed. */ fun onIndexingSettingChanged() {} 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 6c4ab3680..40746dd9c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -53,15 +53,15 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.library) return - val library = musicRepository.library ?: return + if (!changes.deviceLibrary) return + val deviceLibrary = musicRepository.deviceLibrary ?: return _statistics.value = Statistics( - library.songs.size, - library.albums.size, - library.artists.size, - library.genres.size, - library.songs.sumOf { it.durationMs }) + deviceLibrary.songs.size, + deviceLibrary.albums.size, + deviceLibrary.artists.size, + deviceLibrary.genres.size, + deviceLibrary.songs.sumOf { it.durationMs }) } override fun onIndexingStateChanged() { diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index 325736e85..86e9e4de4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -27,7 +27,7 @@ import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters -import org.oxycblt.auxio.music.library.RawSong +import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.metadata.Date import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.splitEscaped diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 8cfa1f28d..967d53d68 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -19,7 +19,7 @@ package org.oxycblt.auxio.music.cache import javax.inject.Inject -import org.oxycblt.auxio.music.library.RawSong +import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.util.* /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt similarity index 74% rename from app/src/main/java/org/oxycblt/auxio/music/library/Library.kt rename to app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 36a5ef7be..a19aec5b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * Library.kt is part of Auxio. + * DeviceLibrary.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,19 +16,20 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.library +package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri import android.provider.OpenableColumns +import javax.inject.Inject import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.storage.contentResolverSafe -import org.oxycblt.auxio.music.storage.useQuery +import org.oxycblt.auxio.music.fs.contentResolverSafe +import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.util.logD /** - * Organized music library information. + * Organized music library information obtained from device storage. * * This class allows for the creation of a well-formed music library graph from raw song * information. It's generally not expected to create this yourself and instead use @@ -36,40 +37,23 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart */ -interface Library { - /** All [Song]s in this [Library]. */ +interface DeviceLibrary { + /** All [Song]s in this [DeviceLibrary]. */ val songs: List - /** All [Album]s in this [Library]. */ + /** All [Album]s in this [DeviceLibrary]. */ val albums: List - /** All [Artist]s in this [Library]. */ + /** All [Artist]s in this [DeviceLibrary]. */ val artists: List - /** All [Genre]s in this [Library]. */ + /** All [Genre]s in this [DeviceLibrary]. */ val genres: List /** - * Finds a [Music] item [T] in the library by it's [Music.UID]. + * Find a [Song] instance corresponding to the given [Music.UID]. * * @param uid The [Music.UID] to search for. - * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or - * the [Music.UID] did not correspond to a [T]. + * @return The corresponding [Song], or null if one was not found. */ - fun find(uid: Music.UID): T? - - /** - * Convert a [Song] from an another library into a [Song] in this [Library]. - * - * @param song The [Song] to convert. - * @return The analogous [Song] in this [Library], or null if it does not exist. - */ - fun sanitize(song: Song): Song? - - /** - * Convert a [MusicParent] from an another library into a [MusicParent] in this [Library]. - * - * @param parent The [MusicParent] to convert. - * @return The analogous [Album] in this [Library], or null if it does not exist. - */ - fun sanitize(parent: T): T? + fun findSong(uid: Music.UID): Song? /** * Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri]. @@ -80,34 +64,72 @@ interface Library { */ fun findSongForUri(context: Context, uri: Uri): Song? + /** + * Find a [Album] instance corresponding to the given [Music.UID]. + * + * @param uid The [Music.UID] to search for. + * @return The corresponding [Song], or null if one was not found. + */ + fun findAlbum(uid: Music.UID): Album? + + /** + * Find a [Artist] instance corresponding to the given [Music.UID]. + * + * @param uid The [Music.UID] to search for. + * @return The corresponding [Song], or null if one was not found. + */ + fun findArtist(uid: Music.UID): Artist? + + /** + * Find a [Genre] instance corresponding to the given [Music.UID]. + * + * @param uid The [Music.UID] to search for. + * @return The corresponding [Song], or null if one was not found. + */ + fun findGenre(uid: Music.UID): Genre? + + /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ + interface Provider { + /** + * Create a new [DeviceLibrary]. + * + * @param rawSongs [RawSong] instances to create a [DeviceLibrary] from. + */ + suspend fun create(rawSongs: List): DeviceLibrary + } + companion object { /** - * Create an instance of [Library]. + * Create an instance of [DeviceLibrary]. * * @param rawSongs [RawSong]s to create the library out of. * @param settings [MusicSettings] required. */ - fun from(rawSongs: List, settings: MusicSettings): Library = - LibraryImpl(rawSongs, settings) + fun from(rawSongs: List, settings: MusicSettings): DeviceLibrary = + DeviceLibraryImpl(rawSongs, settings) } } -private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Library { +class DeviceLibraryProviderImpl @Inject constructor(private val musicSettings: MusicSettings) : + DeviceLibrary.Provider { + override suspend fun create(rawSongs: List): DeviceLibrary = + DeviceLibraryImpl(rawSongs, musicSettings) +} + +private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings) : DeviceLibrary { override val songs = buildSongs(rawSongs, settings) override val albums = buildAlbums(songs, settings) override val artists = buildArtists(songs, albums, settings) override val genres = buildGenres(songs, settings) // Use a mapping to make finding information based on it's UID much faster. - private val uidMap = buildMap { - songs.forEach { put(it.uid, it.finalize()) } - albums.forEach { put(it.uid, it.finalize()) } - artists.forEach { put(it.uid, it.finalize()) } - genres.forEach { put(it.uid, it.finalize()) } - } + private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } + 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()) } } override fun equals(other: Any?) = - other is Library && + other is DeviceLibrary && other.songs == songs && other.albums == albums && other.artists == artists && @@ -121,18 +143,10 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li return hashCode } - /** - * Finds a [Music] item [T] in the library by it's [Music.UID]. - * - * @param uid The [Music.UID] to search for. - * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or - * the [Music.UID] did not correspond to a [T]. - */ - @Suppress("UNCHECKED_CAST") override fun find(uid: Music.UID) = uidMap[uid] as? T - - override fun sanitize(song: Song) = find(song.uid) - - override fun sanitize(parent: T) = find(parent.uid) + override fun findSong(uid: Music.UID) = songUidMap[uid] + override fun findAlbum(uid: Music.UID) = albumUidMap[uid] + override fun findArtist(uid: Music.UID) = artistUidMap[uid] + override fun findGenre(uid: Music.UID) = genreUidMap[uid] override fun findSongForUri(context: Context, uri: Uri) = context.contentResolverSafe.useQuery( diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt new file mode 100644 index 000000000..3bb3de657 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Auxio Project + * DeviceModule.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.music.device + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface DeviceModule { + @Binds + fun deviceLibraryProvider(providerImpl: DeviceLibraryProviderImpl): DeviceLibrary.Provider +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/library/MusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/music/library/MusicImpl.kt rename to app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 975fc7933..f81644d10 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/library/MusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * MusicImpl.kt is part of Auxio. + * DeviceMusicImpl.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.library +package org.oxycblt.auxio.music.device import android.content.Context import androidx.annotation.VisibleForTesting @@ -24,15 +24,15 @@ import java.security.MessageDigest import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.fs.MimeType +import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.fs.toAudioUri +import org.oxycblt.auxio.music.fs.toCoverUri import org.oxycblt.auxio.music.metadata.Date import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.ReleaseType import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseMultiValue -import org.oxycblt.auxio.music.storage.MimeType -import org.oxycblt.auxio.music.storage.Path -import org.oxycblt.auxio.music.storage.toAudioUri -import org.oxycblt.auxio.music.storage.toCoverUri import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -467,7 +467,7 @@ class GenreImpl( * * @return This instance upcasted to [Genre]. */ - fun finalize(): Music { + fun finalize(): Genre { check(songs.isNotEmpty()) { "Malformed genre: Empty" } return this } diff --git a/app/src/main/java/org/oxycblt/auxio/music/library/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/music/library/RawMusic.kt rename to app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index e8acbad7e..86b8df817 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/library/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.library +package org.oxycblt.auxio.music.device import java.util.UUID import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.metadata.* -import org.oxycblt.auxio.music.storage.Directory /** * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt index 7c0117968..5913c2b8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 83369efd4..defbb7c3f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 Auxio Project - * Filesystem.kt is part of Auxio. + * Fs.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.content.Context import android.media.MediaFormat diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt similarity index 92% rename from app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt index 11d0e5650..10c4192bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * StorageModule.kt is part of Auxio. + * FsModule.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.content.Context import dagger.Module @@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.MusicSettings @Module @InstallIn(SingletonComponent::class) -class StorageModule { +class FsModule { @Provides fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) = MediaStoreExtractor.from(context, musicSettings) diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 5ea3a6cbf..f97318d5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.content.Context import android.database.Cursor @@ -31,7 +31,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.cache.Cache -import org.oxycblt.auxio.music.library.RawSong +import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.metadata.Date import org.oxycblt.auxio.music.metadata.parseId3v2PositionField import org.oxycblt.auxio.music.metadata.transformPositionField diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt index fdd59f0d1..4ecea1336 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.content.ActivityNotFoundException import android.net.Uri diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt rename to app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt index 0ed74674e..1a057bd94 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.storage +package org.oxycblt.auxio.music.fs import android.annotation.SuppressLint import android.content.ContentResolver diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt index b31b5f63f..5f801a1e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt @@ -24,7 +24,7 @@ import android.media.MediaFormat import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.storage.MimeType +import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index d6e66c094..60586d9ee 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.MetadataRetriever import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.library.RawSong +import org.oxycblt.auxio.music.device.RawSong /** * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 62f717d9f..04ca06409 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -25,8 +25,8 @@ import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.TrackGroupArray import java.util.concurrent.Future import javax.inject.Inject -import org.oxycblt.auxio.music.library.RawSong -import org.oxycblt.auxio.music.storage.toAudioUri +import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index a12e252fb..72444399c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -34,7 +34,7 @@ import javax.inject.Inject import kotlinx.coroutines.* import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.storage.contentResolverSafe +import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.util.getSystemServiceCompat @@ -131,8 +131,8 @@ class IndexerService : override val scope = indexScope override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.library) return - val library = musicRepository.library ?: return + if (!changes.deviceLibrary) return + val deviceLibrary = musicRepository.deviceLibrary ?: return // Wipe possibly-invalidated outdated covers imageLoader.memoryCache?.clear() // Clear invalid models from PlaybackStateManager. This is not connected @@ -141,10 +141,11 @@ class IndexerService : playbackManager.toSavedState()?.let { savedState -> playbackManager.applySavedState( PlaybackStateManager.SavedState( - parent = savedState.parent?.let(library::sanitize), + parent = + savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent }, queueState = savedState.queueState.remap { song -> - library.sanitize(requireNotNull(song)) + deviceLibrary.findSong(requireNotNull(song).uid) }, positionMs = savedState.positionMs, repeatMode = savedState.repeatMode), diff --git a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistDatabase.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistDatabase.kt rename to app/src/main/java/org/oxycblt/auxio/music/user/PlaylistDatabase.kt index 652f0be74..3377b172a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistDatabase.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.playlist +package org.oxycblt.auxio.music.user import androidx.room.* import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt similarity index 79% rename from app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt rename to app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 848685ac6..e5432e1d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -16,20 +16,23 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.playlist +package org.oxycblt.auxio.music.user import android.content.Context import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.music.device.DeviceLibrary -class PlaylistImpl(rawPlaylist: RawPlaylist, library: Library, musicSettings: MusicSettings) : - Playlist { +class PlaylistImpl( + rawPlaylist: RawPlaylist, + deviceLibrary: DeviceLibrary, + musicSettings: MusicSettings +) : Playlist { override val uid = rawPlaylist.playlistInfo.playlistUid override val rawName = rawPlaylist.playlistInfo.name override fun resolveName(context: Context) = rawName override val rawSortName = null override val sortName = SortName(rawName, musicSettings) - override val songs = rawPlaylist.songs.mapNotNull { library.find(it.songUid) } + override val songs = rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) } override val durationMs = songs.sumOf { it.durationMs } override val albums = songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } diff --git a/app/src/main/java/org/oxycblt/auxio/music/playlist/RawPlaylist.kt b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt similarity index 97% rename from app/src/main/java/org/oxycblt/auxio/music/playlist/RawPlaylist.kt rename to app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt index 58f232f47..6f56be360 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/playlist/RawPlaylist.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.playlist +package org.oxycblt.auxio.music.user import androidx.room.* import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt new file mode 100644 index 000000000..5e1b86860 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Auxio Project + * UserLibrary.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.music.user + +import javax.inject.Inject +import kotlinx.coroutines.channels.Channel +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.device.DeviceLibrary + +/** + * Organized library information controlled by the user. + * + * Unlike [DeviceLibrary], [UserLibrary]s can be mutated without needing to clone the instance. It + * is also not backed by library information, rather an app database with in-memory caching. It is + * generally not expected to create this yourself, and instead rely on MusicRepository. + * + * @author Alexander Capehart + */ +interface UserLibrary { + /** The current user-defined playlists. */ + val playlists: List + + /** + * Find a [Playlist] instance corresponding to the given [Music.UID]. + * + * @param uid The [Music.UID] to search for. + * @return The corresponding [Song], or null if one was not found. + */ + fun findPlaylist(uid: Music.UID): Playlist? + + /** Constructs a [UserLibrary] implementation in an asynchronous manner. */ + interface Provider { + /** + * 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. + */ + suspend fun read(deviceLibrary: Channel): UserLibrary + } +} + +class UserLibraryProviderImpl +@Inject +constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) : + UserLibrary.Provider { + override suspend fun read(deviceLibrary: Channel): UserLibrary = + UserLibraryImpl(playlistDao, deviceLibrary.receive(), musicSettings) +} + +private class UserLibraryImpl( + private val playlistDao: PlaylistDao, + private val deviceLibrary: DeviceLibrary, + private val musicSettings: MusicSettings +) : UserLibrary { + private val playlistMap = mutableMapOf() + override val playlists: List + get() = playlistMap.values.toList() + + override fun findPlaylist(uid: Music.UID) = playlistMap[uid] +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistModule.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt similarity index 83% rename from app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistModule.kt rename to app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt index 45da01dc7..9c92c0ca5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * PlaylistModule.kt is part of Auxio. + * UserModule.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,10 +16,11 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.playlist +package org.oxycblt.auxio.music.user import android.content.Context import androidx.room.Room +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,7 +29,13 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -class PlaylistModule { +interface UserModule { + @Binds fun userLibaryProvider(provider: UserLibraryProviderImpl): UserLibrary.Provider +} + +@Module +@InstallIn(SingletonComponent::class) +class UserRoomModule { @Provides fun playlistDao(database: PlaylistDatabase) = database.playlistDao() @Provides diff --git a/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt index cda23118c..0ddb37953 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt @@ -24,7 +24,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.util.unlikelyToBeNull /** * a [ViewModel] that manages the current music picker state. Make it so that the dialogs just @@ -60,7 +59,7 @@ class PickerViewModel @Inject constructor(private val musicRepository: MusicRepo } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (changes.library && musicRepository.library != null) { + if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { refreshChoices() } } @@ -71,8 +70,7 @@ class PickerViewModel @Inject constructor(private val musicRepository: MusicRepo * @param uid The [Music.UID] of the [Song] to update to. */ fun setItemUid(uid: Music.UID) { - val library = unlikelyToBeNull(musicRepository.library) - _currentItem.value = library.find(uid) + _currentItem.value = musicRepository.find(uid) refreshChoices() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 23c7af7b6..f5f177005 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -282,7 +282,7 @@ constructor( check(song == null || parent == null || parent.songs.contains(song)) { "Song to play not in parent" } - val library = musicRepository.library ?: return + val deviceLibrary = musicRepository.deviceLibrary ?: return val sort = when (parent) { is Genre -> musicSettings.genreSongSort @@ -291,7 +291,7 @@ constructor( is Playlist -> TODO("handle this") null -> musicSettings.songSort } - val queue = sort.songs(parent?.songs ?: library.songs) + val queue = sort.songs(parent?.songs ?: deviceLibrary.songs) playbackManager.play(song, parent, queue, shuffled) } @@ -469,14 +469,11 @@ constructor( */ fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { viewModelScope.launch { - val library = musicRepository.library - if (library != null) { - val savedState = persistenceRepository.readState(library) - if (savedState != null) { - playbackManager.applySavedState(savedState, true) - onDone(true) - return@launch - } + val savedState = persistenceRepository.readState() + if (savedState != null) { + playbackManager.applySavedState(savedState, true) + onDone(true) + return@launch } onDone(false) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index 38cbba829..a246689fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.playback.persist import javax.inject.Inject import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD @@ -32,12 +32,8 @@ import org.oxycblt.auxio.util.logE * @author Alexander Capehart (OxygenCobalt) */ interface PersistenceRepository { - /** - * Read the previously persisted [PlaybackStateManager.SavedState]. - * - * @param library The [Library] required to de-serialize the [PlaybackStateManager.SavedState]. - */ - suspend fun readState(library: Library): PlaybackStateManager.SavedState? + /** Read the previously persisted [PlaybackStateManager.SavedState]. */ + suspend fun readState(): PlaybackStateManager.SavedState? /** * Persist a new [PlaybackStateManager.SavedState]. @@ -49,10 +45,14 @@ interface PersistenceRepository { class PersistenceRepositoryImpl @Inject -constructor(private val playbackStateDao: PlaybackStateDao, private val queueDao: QueueDao) : - PersistenceRepository { +constructor( + private val playbackStateDao: PlaybackStateDao, + private val queueDao: QueueDao, + private val musicRepository: MusicRepository +) : PersistenceRepository { - override suspend fun readState(library: Library): PlaybackStateManager.SavedState? { + override suspend fun readState(): PlaybackStateManager.SavedState? { + val deviceLibrary = musicRepository.deviceLibrary ?: return null val playbackState: PlaybackState val heap: List val mapping: List @@ -73,14 +73,14 @@ constructor(private val playbackStateDao: PlaybackStateDao, private val queueDao shuffledMapping.add(entry.shuffledIndex) } - val parent = playbackState.parentUid?.let { library.find(it) } + val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } logD("Read playback state") return PlaybackStateManager.SavedState( parent = parent, queueState = Queue.SavedState( - heap.map { library.find(it.uid) }, + heap.map { deviceLibrary.findSong(it.uid) }, orderedMapping, shuffledMapping, playbackState.index, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 5cc4c89fb..a03f231fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -299,7 +299,7 @@ class PlaybackService : } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (changes.library && musicRepository.library != null) { + if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { // We now have a library, see if we have anything we need to do. playbackManager.requestAction(this) } @@ -328,8 +328,8 @@ class PlaybackService : } override fun performAction(action: InternalPlayer.Action): Boolean { - val library = - musicRepository.library + val deviceLibrary = + musicRepository.deviceLibrary // No library, cannot do anything. ?: return false @@ -339,22 +339,23 @@ class PlaybackService : // Restore state -> Start a new restoreState job is InternalPlayer.Action.RestoreState -> { restoreScope.launch { - persistenceRepository.readState(library)?.let { + persistenceRepository.readState()?.let { playbackManager.applySavedState(it, false) } } } // Shuffle all -> Start new playback from all songs is InternalPlayer.Action.ShuffleAll -> { - playbackManager.play(null, null, musicSettings.songSort.songs(library.songs), true) + playbackManager.play( + null, null, musicSettings.songSort.songs(deviceLibrary.songs), true) } // Open -> Try to find the Song for the given file and then play it from all songs is InternalPlayer.Action.Open -> { - library.findSongForUri(application, action.uri)?.let { song -> + deviceLibrary.findSongForUri(application, action.uri)?.let { song -> playbackManager.play( song, null, - musicSettings.songSort.songs(library.songs), + musicSettings.songSort.songs(deviceLibrary.songs), playbackManager.queue.isShuffled && playbackSettings.keepShuffle) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 99f857793..68a0b3b7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.logD @@ -73,7 +73,7 @@ constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (changes.library && musicRepository.library != null) { + if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { search(lastQuery) } } @@ -89,8 +89,8 @@ constructor( currentSearchJob?.cancel() lastQuery = query - val library = musicRepository.library - if (query.isNullOrEmpty() || library == null) { + val deviceLibrary = musicRepository.deviceLibrary + if (query.isNullOrEmpty() || deviceLibrary == null) { logD("Search query is not applicable.") _searchResults.value = listOf() return @@ -101,23 +101,27 @@ constructor( // Searching is time-consuming, so do it in the background. currentSearchJob = viewModelScope.launch { - _searchResults.value = searchImpl(library, query).also { yield() } + _searchResults.value = searchImpl(deviceLibrary, query).also { yield() } } } - private suspend fun searchImpl(library: Library, query: String): List { + private suspend fun searchImpl(deviceLibrary: DeviceLibrary, query: String): List { val filterMode = searchSettings.searchFilterMode val items = if (filterMode == null) { // A nulled filter mode means to not filter anything. - SearchEngine.Items(library.songs, library.albums, library.artists, library.genres) + SearchEngine.Items( + deviceLibrary.songs, + deviceLibrary.albums, + deviceLibrary.artists, + deviceLibrary.genres) } else { SearchEngine.Items( - songs = if (filterMode == MusicMode.SONGS) library.songs else null, - albums = if (filterMode == MusicMode.ALBUMS) library.albums else null, - artists = if (filterMode == MusicMode.ARTISTS) library.artists else null, - genres = if (filterMode == MusicMode.GENRES) library.genres else null) + songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null, + albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null, + artists = if (filterMode == MusicMode.ARTISTS) deviceLibrary.artists else null, + genres = if (filterMode == MusicMode.GENRES) deviceLibrary.genres else null) } val results = searchEngine.search(items, query) diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 42ea52268..69a13a74c 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -143,7 +143,7 @@ tools:layout="@layout/dialog_pre_amp" /> ? + override val userLibrary: UserLibrary? get() = throw NotImplementedError() - set(_) { - throw NotImplementedError() - } override fun addUpdateListener(listener: MusicRepository.UpdateListener) { throw NotImplementedError() @@ -62,6 +54,10 @@ open class FakeMusicRepository : MusicRepository { throw NotImplementedError() } + override fun find(uid: Music.UID): Music? { + throw NotImplementedError() + } + override fun requestIndex(withCache: Boolean) { throw NotImplementedError() } diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt index 07c5166c9..83b6f7e2d 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt @@ -19,7 +19,7 @@ package org.oxycblt.auxio.music import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.storage.MusicDirectories +import org.oxycblt.auxio.music.fs.MusicDirectories open class FakeMusicSettings : MusicSettings { override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError() diff --git a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt b/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt index 92b2534b0..b1540283e 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.music import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import org.oxycblt.auxio.music.library.FakeLibrary -import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.device.FakeDeviceLibrary import org.oxycblt.auxio.util.forceClear class MusicViewModelTest { @@ -49,7 +49,7 @@ class MusicViewModelTest { val musicRepository = TestMusicRepository() val musicViewModel = MusicViewModel(musicRepository) assertEquals(null, musicViewModel.statistics.value) - musicRepository.library = TestLibrary() + musicRepository.deviceLibrary = TestDeviceLibrary() assertEquals( MusicViewModel.Statistics( 2, @@ -71,11 +71,11 @@ class MusicViewModelTest { } private class TestMusicRepository : FakeMusicRepository() { - override var library: Library? = null + override var deviceLibrary: DeviceLibrary? = null set(value) { field = value updateListener?.onMusicChanges( - MusicRepository.Changes(library = true, playlists = false)) + MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } override var indexingState: IndexingState? = null set(value) { @@ -88,7 +88,8 @@ class MusicViewModelTest { val requests = mutableListOf() override fun addUpdateListener(listener: MusicRepository.UpdateListener) { - listener.onMusicChanges(MusicRepository.Changes(library = true, playlists = false)) + listener.onMusicChanges( + MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) this.updateListener = listener } @@ -110,7 +111,7 @@ class MusicViewModelTest { } } - private class TestLibrary : FakeLibrary() { + private class TestDeviceLibrary : FakeDeviceLibrary() { override val songs: List get() = listOf(TestSong(), TestSong()) override val albums: List diff --git a/app/src/test/java/org/oxycblt/auxio/music/library/FakeLibrary.kt b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt similarity index 77% rename from app/src/test/java/org/oxycblt/auxio/music/library/FakeLibrary.kt rename to app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt index 7230e5e76..93cbaa62c 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/library/FakeLibrary.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * FakeLibrary.kt is part of Auxio. + * FakeDeviceLibrary.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.library +package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri import org.oxycblt.auxio.music.* -open class FakeLibrary : Library { +open class FakeDeviceLibrary : DeviceLibrary { override val songs: List get() = throw NotImplementedError() override val albums: List @@ -32,7 +32,7 @@ open class FakeLibrary : Library { override val genres: List get() = throw NotImplementedError() - override fun find(uid: Music.UID): T? { + override fun findSong(uid: Music.UID): Song? { throw NotImplementedError() } @@ -40,11 +40,15 @@ open class FakeLibrary : Library { throw NotImplementedError() } - override fun sanitize(parent: T): T? { + override fun findAlbum(uid: Music.UID): Album? { throw NotImplementedError() } - override fun sanitize(song: Song): Song? { + override fun findArtist(uid: Music.UID): Artist? { + throw NotImplementedError() + } + + override fun findGenre(uid: Music.UID): Genre? { throw NotImplementedError() } } diff --git a/app/src/test/java/org/oxycblt/auxio/music/library/RawMusicTest.kt b/app/src/test/java/org/oxycblt/auxio/music/device/RawMusicTest.kt similarity index 99% rename from app/src/test/java/org/oxycblt/auxio/music/library/RawMusicTest.kt rename to app/src/test/java/org/oxycblt/auxio/music/device/RawMusicTest.kt index a4fafb324..99d5a148b 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/library/RawMusicTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/RawMusicTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.library +package org.oxycblt.auxio.music.device import java.util.* import org.junit.Assert.assertEquals