music: refactor library usage
Refactor the disjoint Library and Playlist setup into two new DeviceLibrary and UserLibrary implementations. This makes the API surface a bit less disjoint than prior.
This commit is contained in:
parent
d8b67a8512
commit
f846a08b01
41 changed files with 398 additions and 242 deletions
|
@ -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<Song>(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<Album>(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<Artist>(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<Genre>(uid)?.also(::refreshGenreList)
|
||||
_currentGenre.value =
|
||||
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList)
|
||||
}
|
||||
|
||||
private fun <T : Music> requireMusic(uid: Music.UID) = musicRepository.library?.find<T>(uid)
|
||||
|
||||
private fun refreshAudioInfo(song: Song) {
|
||||
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
||||
currentSongJob?.cancel()
|
||||
|
|
|
@ -228,8 +228,7 @@ class GenreDetailFragment :
|
|||
is Genre -> {
|
||||
navModel.exploreNavigationItem.consume()
|
||||
}
|
||||
is Playlist -> TODO("handle this")
|
||||
null -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Playlist>?
|
||||
/** 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<MusicRepository.UpdateListener>()
|
||||
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
||||
private var indexingWorker: MusicRepository.IndexingWorker? = null
|
||||
|
||||
override var library: Library? = null
|
||||
override var playlists: List<Playlist>? = 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<DeviceLibrary>()
|
||||
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<Playlist>) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<MusicSettings.Listener> {
|
|||
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() {}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.*
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Song>
|
||||
/** All [Album]s in this [Library]. */
|
||||
/** All [Album]s in this [DeviceLibrary]. */
|
||||
val albums: List<Album>
|
||||
/** All [Artist]s in this [Library]. */
|
||||
/** All [Artist]s in this [DeviceLibrary]. */
|
||||
val artists: List<Artist>
|
||||
/** All [Genre]s in this [Library]. */
|
||||
/** All [Genre]s in this [DeviceLibrary]. */
|
||||
val genres: List<Genre>
|
||||
|
||||
/**
|
||||
* 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 <T : Music> 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 <T : MusicParent> 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<RawSong>): 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<RawSong>, settings: MusicSettings): Library =
|
||||
LibraryImpl(rawSongs, settings)
|
||||
fun from(rawSongs: List<RawSong>, settings: MusicSettings): DeviceLibrary =
|
||||
DeviceLibraryImpl(rawSongs, settings)
|
||||
}
|
||||
}
|
||||
|
||||
private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Library {
|
||||
class DeviceLibraryProviderImpl @Inject constructor(private val musicSettings: MusicSettings) :
|
||||
DeviceLibrary.Provider {
|
||||
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary =
|
||||
DeviceLibraryImpl(rawSongs, musicSettings)
|
||||
}
|
||||
|
||||
private class DeviceLibraryImpl(rawSongs: List<RawSong>, 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<RawSong>, 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 <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
|
||||
|
||||
override fun sanitize(song: Song) = find<Song>(song.uid)
|
||||
|
||||
override fun <T : MusicParent> sanitize(parent: T) = find<T>(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(
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.storage
|
||||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.storage
|
||||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaFormat
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.storage
|
||||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.storage
|
||||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.playlist
|
||||
package org.oxycblt.auxio.music.user
|
||||
|
||||
import androidx.room.*
|
||||
import org.oxycblt.auxio.music.Music
|
|
@ -16,20 +16,23 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Song>(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 }
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.playlist
|
||||
package org.oxycblt.auxio.music.user
|
||||
|
||||
import androidx.room.*
|
||||
import org.oxycblt.auxio.music.Music
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Playlist>
|
||||
|
||||
/**
|
||||
* 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<DeviceLibrary>): UserLibrary
|
||||
}
|
||||
}
|
||||
|
||||
class UserLibraryProviderImpl
|
||||
@Inject
|
||||
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
||||
UserLibrary.Provider {
|
||||
override suspend fun read(deviceLibrary: Channel<DeviceLibrary>): 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<Music.UID, PlaylistImpl>()
|
||||
override val playlists: List<Playlist>
|
||||
get() = playlistMap.values.toList()
|
||||
|
||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<QueueHeapItem>
|
||||
val mapping: List<QueueMappingItem>
|
||||
|
@ -73,14 +73,14 @@ constructor(private val playbackStateDao: PlaybackStateDao, private val queueDao
|
|||
shuffledMapping.add(entry.shuffledIndex)
|
||||
}
|
||||
|
||||
val parent = playbackState.parentUid?.let { library.find<MusicParent>(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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Item> {
|
||||
private suspend fun searchImpl(deviceLibrary: DeviceLibrary, query: String): List<Item> {
|
||||
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)
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
tools:layout="@layout/dialog_pre_amp" />
|
||||
<dialog
|
||||
android:id="@+id/music_dirs_dialog"
|
||||
android:name="org.oxycblt.auxio.music.storage.MusicDirsDialog"
|
||||
android:name="org.oxycblt.auxio.music.fs.MusicDirsDialog"
|
||||
android:label="music_dirs_dialog"
|
||||
tools:layout="@layout/dialog_music_dirs" />
|
||||
<dialog
|
||||
|
|
|
@ -20,12 +20,11 @@ package org.oxycblt.auxio.music
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.oxycblt.auxio.music.*
|
||||
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
|
||||
|
||||
open class FakeSong : Song {
|
||||
override val rawName: String?
|
||||
|
|
|
@ -19,24 +19,16 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import org.oxycblt.auxio.music.library.Library
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
import org.oxycblt.auxio.music.user.UserLibrary
|
||||
|
||||
open class FakeMusicRepository : MusicRepository {
|
||||
override var indexingState: IndexingState?
|
||||
override val indexingState: IndexingState?
|
||||
get() = throw NotImplementedError()
|
||||
set(_) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
override var library: Library?
|
||||
override val deviceLibrary: DeviceLibrary?
|
||||
get() = throw NotImplementedError()
|
||||
set(_) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
override var playlists: List<Playlist>?
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Boolean>()
|
||||
|
||||
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<Song>
|
||||
get() = listOf(TestSong(), TestSong())
|
||||
override val albums: List<Album>
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Song>
|
||||
get() = throw NotImplementedError()
|
||||
override val albums: List<Album>
|
||||
|
@ -32,7 +32,7 @@ open class FakeLibrary : Library {
|
|||
override val genres: List<Genre>
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
override fun <T : Music> 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 <T : MusicParent> 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()
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.library
|
||||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import java.util.*
|
||||
import org.junit.Assert.assertEquals
|
Loading…
Reference in a new issue