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:
Alexander Capehart 2023-03-22 17:09:33 -06:00
parent d8b67a8512
commit f846a08b01
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
41 changed files with 398 additions and 242 deletions

View file

@ -159,8 +159,8 @@ constructor(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.library) return if (!changes.deviceLibrary) return
val library = musicRepository.library ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
// If we are showing any item right now, we will need to refresh it (and any information // 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 // 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 val song = currentSong.value
if (song != null) { if (song != null) {
_currentSong.value = library.sanitize(song)?.also(::refreshAudioInfo) _currentSong.value = deviceLibrary.findSong(song.uid)?.also(::refreshAudioInfo)
logD("Updated song to ${currentSong.value}") logD("Updated song to ${currentSong.value}")
} }
val album = currentAlbum.value val album = currentAlbum.value
if (album != null) { if (album != null) {
_currentAlbum.value = library.sanitize(album)?.also(::refreshAlbumList) _currentAlbum.value = deviceLibrary.findAlbum(album.uid)?.also(::refreshAlbumList)
logD("Updated genre to ${currentAlbum.value}") logD("Updated genre to ${currentAlbum.value}")
} }
val artist = currentArtist.value val artist = currentArtist.value
if (artist != null) { if (artist != null) {
_currentArtist.value = library.sanitize(artist)?.also(::refreshArtistList) _currentArtist.value = deviceLibrary.findArtist(artist.uid)?.also(::refreshArtistList)
logD("Updated genre to ${currentArtist.value}") logD("Updated genre to ${currentArtist.value}")
} }
val genre = currentGenre.value val genre = currentGenre.value
if (genre != null) { if (genre != null) {
_currentGenre.value = library.sanitize(genre)?.also(::refreshGenreList) _currentGenre.value = deviceLibrary.findGenre(genre.uid)?.also(::refreshGenreList)
logD("Updated genre to ${currentGenre.value}") logD("Updated genre to ${currentGenre.value}")
} }
} }
@ -203,7 +203,7 @@ constructor(
return return
} }
logD("Opening Song [uid: $uid]") 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 return
} }
logD("Opening Album [uid: $uid]") 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 return
} }
logD("Opening Artist [uid: $uid]") 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 return
} }
logD("Opening Genre [uid: $uid]") 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) { private fun refreshAudioInfo(song: Song) {
// Clear any previous job in order to avoid stale data from appearing in the UI. // Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel() currentSongJob?.cancel()

View file

@ -228,8 +228,7 @@ class GenreDetailFragment :
is Genre -> { is Genre -> {
navModel.exploreNavigationItem.consume() navModel.exploreNavigationItem.consume()
} }
is Playlist -> TODO("handle this") else -> {}
null -> {}
} }
} }

View file

@ -136,33 +136,33 @@ constructor(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val library = musicRepository.library val deviceLibrary = musicRepository.deviceLibrary
if (changes.library && library != null) { if (changes.deviceLibrary && deviceLibrary != null) {
logD("Refreshing library") logD("Refreshing library")
// Get the each list of items in the library to use as our list data. // Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them. // Applying the preferred sorting to them.
_songsInstructions.put(UpdateInstructions.Diff) _songsInstructions.put(UpdateInstructions.Diff)
_songsList.value = musicSettings.songSort.songs(library.songs) _songsList.value = musicSettings.songSort.songs(deviceLibrary.songs)
_albumsInstructions.put(UpdateInstructions.Diff) _albumsInstructions.put(UpdateInstructions.Diff)
_albumsLists.value = musicSettings.albumSort.albums(library.albums) _albumsLists.value = musicSettings.albumSort.albums(deviceLibrary.albums)
_artistsInstructions.put(UpdateInstructions.Diff) _artistsInstructions.put(UpdateInstructions.Diff)
_artistsList.value = _artistsList.value =
musicSettings.artistSort.artists( musicSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
// Hide Collaborators is enabled, filter out collaborators. // Hide Collaborators is enabled, filter out collaborators.
library.artists.filter { !it.isCollaborator } deviceLibrary.artists.filter { !it.isCollaborator }
} else { } else {
library.artists deviceLibrary.artists
}) })
_genresInstructions.put(UpdateInstructions.Diff) _genresInstructions.put(UpdateInstructions.Diff)
_genresList.value = musicSettings.genreSort.genres(library.genres) _genresList.value = musicSettings.genreSort.genres(deviceLibrary.genres)
} }
val playlists = musicRepository.playlists val userLibrary = musicRepository.userLibrary
if (changes.playlists && playlists != null) { if (changes.userLibrary && userLibrary != null) {
logD("Refreshing playlists") logD("Refreshing playlists")
_playlistsInstructions.put(UpdateInstructions.Diff) _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() { override fun onHideCollaboratorsChanged() {
// Changes in the hide collaborator setting will change the artist contents // Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update. // of the library, consider it a library update.
onMusicChanges(MusicRepository.Changes(library = true, playlists = false)) onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false))
} }
/** /**

View file

@ -43,18 +43,19 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.library) return if (!changes.deviceLibrary) return
val library = musicRepository.library ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
val userLibrary = musicRepository.userLibrary ?: return
// Sanitize the selection to remove items that no longer exist and thus // Sanitize the selection to remove items that no longer exist and thus
// won't appear in any list. // won't appear in any list.
_selected.value = _selected.value =
_selected.value.mapNotNull { _selected.value.mapNotNull {
when (it) { when (it) {
is Song -> library.sanitize(it) is Song -> deviceLibrary.findSong(it.uid)
is Album -> library.sanitize(it) is Album -> deviceLibrary.findAlbum(it.uid)
is Artist -> library.sanitize(it) is Artist -> deviceLibrary.findArtist(it.uid)
is Genre -> library.sanitize(it) is Genre -> deviceLibrary.findGenre(it.uid)
is Playlist -> TODO("handle this") is Playlist -> userLibrary.findPlaylist(it.uid)
} }
} }
} }

View file

@ -30,11 +30,11 @@ import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.list.Item 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.Date
import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.Disc
import org.oxycblt.auxio.music.metadata.ReleaseType 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.concatLocalized
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
@ -139,10 +139,10 @@ sealed interface Music : Item {
object TypeConverters { object TypeConverters {
/** @see [Music.UID.toString] */ /** @see [Music.UID.toString] */
@TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString() @TypeConverter fun fromMusicUID(uid: UID?) = uid?.toString()
/** @see [Music.UID.fromString] */ /** @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 { companion object {

View file

@ -26,10 +26,11 @@ import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.library.RawSong 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.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.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -43,10 +44,10 @@ import org.oxycblt.auxio.util.logW
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface MusicRepository { interface MusicRepository {
/** The current immutable music library loaded from the file-system. */ /** The current music information found on the device. */
val library: Library? val deviceLibrary: DeviceLibrary?
/** The current mutable user-defined playlists loaded from the file-system. */ /** The current user-defined music information. */
val playlists: List<Playlist>? val userLibrary: UserLibrary?
/** The current state of music loading. Null if no load has occurred yet. */ /** The current state of music loading. Null if no load has occurred yet. */
val indexingState: IndexingState? val indexingState: IndexingState?
@ -96,6 +97,16 @@ interface MusicRepository {
*/ */
fun unregisterWorker(worker: IndexingWorker) 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 * Request that a music loading operation is started by the current [IndexingWorker]. Does
* nothing if one is not available. * nothing if one is not available.
@ -118,7 +129,7 @@ interface MusicRepository {
/** /**
* Called when a change to the stored music information occurs. * 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) fun onMusicChanges(changes: Changes)
} }
@ -126,10 +137,10 @@ interface MusicRepository {
/** /**
* Flags indicating which kinds of music information changed. * Flags indicating which kinds of music information changed.
* *
* @param library Whether the current [Library] has changed. * @param deviceLibrary Whether the current [DeviceLibrary] has changed.
* @param playlists Whether the current [Playlist]s have 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. */ /** A listener for events in the music loading process. */
interface IndexingListener { interface IndexingListener {
@ -158,17 +169,18 @@ interface MusicRepository {
class MusicRepositoryImpl class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
private val musicSettings: MusicSettings,
private val cacheRepository: CacheRepository, private val cacheRepository: CacheRepository,
private val mediaStoreExtractor: MediaStoreExtractor, private val mediaStoreExtractor: MediaStoreExtractor,
private val tagExtractor: TagExtractor private val tagExtractor: TagExtractor,
private val deviceLibraryProvider: DeviceLibrary.Provider,
private val userLibraryProvider: UserLibrary.Provider
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>() private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
private var indexingWorker: MusicRepository.IndexingWorker? = null private var indexingWorker: MusicRepository.IndexingWorker? = null
override var library: Library? = null override var deviceLibrary: DeviceLibrary? = null
override var playlists: List<Playlist>? = null override var userLibrary: UserLibrary? = null
private var previousCompletedState: IndexingState.Completed? = null private var previousCompletedState: IndexingState.Completed? = null
private var currentIndexingState: IndexingState? = null private var currentIndexingState: IndexingState? = null
override val indexingState: IndexingState? override val indexingState: IndexingState?
@ -216,6 +228,10 @@ constructor(
currentIndexingState = null 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) { override fun requestIndex(withCache: Boolean) {
indexingWorker?.requestIndex(withCache) indexingWorker?.requestIndex(withCache)
} }
@ -295,16 +311,20 @@ constructor(
// parallel. // parallel.
logD("Discovered ${rawSongs.size} songs, starting finalization") logD("Discovered ${rawSongs.size} songs, starting finalization")
emitLoading(IndexingProgress.Indeterminate) emitLoading(IndexingProgress.Indeterminate)
val libraryJob = val deviceLibraryChannel = Channel<DeviceLibrary>()
worker.scope.async(Dispatchers.Main) { Library.from(rawSongs, musicSettings) } 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) { if (cache == null || cache.invalidated) {
cacheRepository.writeCache(rawSongs) cacheRepository.writeCache(rawSongs)
} }
val newLibrary = libraryJob.await() val deviceLibrary = deviceLibraryJob.await()
// TODO: Make real playlist reading val userLibrary = userLibraryJob.await()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
emitComplete(null) emitComplete(null)
emitData(newLibrary, listOf()) emitData(deviceLibrary, userLibrary)
} }
} }
@ -330,14 +350,14 @@ constructor(
} }
@Synchronized @Synchronized
private fun emitData(library: Library, playlists: List<Playlist>) { private fun emitData(deviceLibrary: DeviceLibrary, userLibrary: UserLibrary) {
val libraryChanged = this.library != library val deviceLibraryChanged = this.deviceLibrary != deviceLibrary
val playlistsChanged = this.playlists != playlists val userLibraryChanged = this.userLibrary != userLibrary
if (!libraryChanged && !playlistsChanged) return if (!deviceLibraryChanged && !userLibraryChanged) return
this.library = library this.deviceLibrary = deviceLibrary
this.playlists = playlists this.userLibrary = userLibrary
val changes = MusicRepository.Changes(libraryChanged, playlistsChanged) val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged)
for (listener in updateListeners) { for (listener in updateListeners) {
listener.onMusicChanges(changes) listener.onMusicChanges(changes)
} }

View file

@ -25,8 +25,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.storage.Directory import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.storage.MusicDirectories import org.oxycblt.auxio.music.fs.MusicDirectories
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
@ -62,7 +62,7 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
var artistSongSort: Sort var artistSongSort: Sort
/** The [Sort] mode used in an [Genre]'s [Song] list. */ /** The [Sort] mode used in an [Genre]'s [Song] list. */
var genreSongSort: Sort var genreSongSort: Sort
/** The [] */
interface Listener { interface Listener {
/** Called when a setting controlling how music is loaded has changed. */ /** Called when a setting controlling how music is loaded has changed. */
fun onIndexingSettingChanged() {} fun onIndexingSettingChanged() {}

View file

@ -53,15 +53,15 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.library) return if (!changes.deviceLibrary) return
val library = musicRepository.library ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
_statistics.value = _statistics.value =
Statistics( Statistics(
library.songs.size, deviceLibrary.songs.size,
library.albums.size, deviceLibrary.albums.size,
library.artists.size, deviceLibrary.artists.size,
library.genres.size, deviceLibrary.genres.size,
library.songs.sumOf { it.durationMs }) deviceLibrary.songs.sumOf { it.durationMs })
} }
override fun onIndexingStateChanged() { override fun onIndexingStateChanged() {

View file

@ -27,7 +27,7 @@ import androidx.room.Query
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters 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.Date
import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.metadata.splitEscaped import org.oxycblt.auxio.music.metadata.splitEscaped

View file

@ -19,7 +19,7 @@
package org.oxycblt.auxio.music.cache package org.oxycblt.auxio.music.cache
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.library.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * 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/>. * 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.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import javax.inject.Inject
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.util.logD 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 * 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 * 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 * @author Alexander Capehart
*/ */
interface Library { interface DeviceLibrary {
/** All [Song]s in this [Library]. */ /** All [Song]s in this [DeviceLibrary]. */
val songs: List<Song> val songs: List<Song>
/** All [Album]s in this [Library]. */ /** All [Album]s in this [DeviceLibrary]. */
val albums: List<Album> val albums: List<Album>
/** All [Artist]s in this [Library]. */ /** All [Artist]s in this [DeviceLibrary]. */
val artists: List<Artist> val artists: List<Artist>
/** All [Genre]s in this [Library]. */ /** All [Genre]s in this [DeviceLibrary]. */
val genres: List<Genre> 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. * @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 * @return The corresponding [Song], or null if one was not found.
* the [Music.UID] did not correspond to a [T].
*/ */
fun <T : Music> find(uid: Music.UID): T? fun findSong(uid: Music.UID): Song?
/**
* 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?
/** /**
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri]. * 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? 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 { companion object {
/** /**
* Create an instance of [Library]. * Create an instance of [DeviceLibrary].
* *
* @param rawSongs [RawSong]s to create the library out of. * @param rawSongs [RawSong]s to create the library out of.
* @param settings [MusicSettings] required. * @param settings [MusicSettings] required.
*/ */
fun from(rawSongs: List<RawSong>, settings: MusicSettings): Library = fun from(rawSongs: List<RawSong>, settings: MusicSettings): DeviceLibrary =
LibraryImpl(rawSongs, settings) 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 songs = buildSongs(rawSongs, settings)
override val albums = buildAlbums(songs, settings) override val albums = buildAlbums(songs, settings)
override val artists = buildArtists(songs, albums, settings) override val artists = buildArtists(songs, albums, settings)
override val genres = buildGenres(songs, settings) override val genres = buildGenres(songs, settings)
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val uidMap = buildMap { private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
songs.forEach { put(it.uid, it.finalize()) } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
albums.forEach { put(it.uid, it.finalize()) } private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } }
artists.forEach { put(it.uid, it.finalize()) } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } }
genres.forEach { put(it.uid, it.finalize()) }
}
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Library && other is DeviceLibrary &&
other.songs == songs && other.songs == songs &&
other.albums == albums && other.albums == albums &&
other.artists == artists && other.artists == artists &&
@ -121,18 +143,10 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
return hashCode return hashCode
} }
/** override fun findSong(uid: Music.UID) = songUidMap[uid]
* Finds a [Music] item [T] in the library by it's [Music.UID]. override fun findAlbum(uid: Music.UID) = albumUidMap[uid]
* override fun findArtist(uid: Music.UID) = artistUidMap[uid]
* @param uid The [Music.UID] to search for. override fun findGenre(uid: Music.UID) = genreUidMap[uid]
* @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 findSongForUri(context: Context, uri: Uri) = override fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(

View file

@ -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
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * 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/>. * 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.content.Context
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -24,15 +24,15 @@ import java.security.MessageDigest
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.* 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.Date
import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.Disc
import org.oxycblt.auxio.music.metadata.ReleaseType import org.oxycblt.auxio.music.metadata.ReleaseType
import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseId3GenreNames
import org.oxycblt.auxio.music.metadata.parseMultiValue 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.nonZeroOrNull
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -467,7 +467,7 @@ class GenreImpl(
* *
* @return This instance upcasted to [Genre]. * @return This instance upcasted to [Genre].
*/ */
fun finalize(): Music { fun finalize(): Genre {
check(songs.isNotEmpty()) { "Malformed genre: Empty" } check(songs.isNotEmpty()) { "Malformed genre: Empty" }
return this return this
} }

View file

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.library package org.oxycblt.auxio.music.device
import java.util.UUID import java.util.UUID
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.metadata.* import org.oxycblt.auxio.music.metadata.*
import org.oxycblt.auxio.music.storage.Directory
/** /**
* Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances.

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.storage package org.oxycblt.auxio.music.fs
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * 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 * 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 * 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/>. * 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.content.Context
import android.media.MediaFormat import android.media.MediaFormat

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * 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/>. * 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.content.Context
import dagger.Module import dagger.Module
@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.MusicSettings
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class StorageModule { class FsModule {
@Provides @Provides
fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) = fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) =
MediaStoreExtractor.from(context, musicSettings) MediaStoreExtractor.from(context, musicSettings)

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.storage package org.oxycblt.auxio.music.fs
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -31,7 +31,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.cache.Cache 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.Date
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
import org.oxycblt.auxio.music.metadata.transformPositionField import org.oxycblt.auxio.music.metadata.transformPositionField

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.storage package org.oxycblt.auxio.music.fs
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.net.Uri import android.net.Uri

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.storage package org.oxycblt.auxio.music.fs
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver

View file

@ -24,7 +24,7 @@ import android.media.MediaFormat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.Song 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.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW

View file

@ -22,7 +22,7 @@ import com.google.android.exoplayer2.MetadataRetriever
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield 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 * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the

View file

@ -25,8 +25,8 @@ import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.TrackGroupArray import com.google.android.exoplayer2.source.TrackGroupArray
import java.util.concurrent.Future import java.util.concurrent.Future
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.library.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.storage.toAudioUri import org.oxycblt.auxio.music.fs.toAudioUri
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW

View file

@ -34,7 +34,7 @@ import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.* 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.playback.state.PlaybackStateManager
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
@ -131,8 +131,8 @@ class IndexerService :
override val scope = indexScope override val scope = indexScope
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.library) return if (!changes.deviceLibrary) return
val library = musicRepository.library ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
// Wipe possibly-invalidated outdated covers // Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
// Clear invalid models from PlaybackStateManager. This is not connected // Clear invalid models from PlaybackStateManager. This is not connected
@ -141,10 +141,11 @@ class IndexerService :
playbackManager.toSavedState()?.let { savedState -> playbackManager.toSavedState()?.let { savedState ->
playbackManager.applySavedState( playbackManager.applySavedState(
PlaybackStateManager.SavedState( PlaybackStateManager.SavedState(
parent = savedState.parent?.let(library::sanitize), parent =
savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent },
queueState = queueState =
savedState.queueState.remap { song -> savedState.queueState.remap { song ->
library.sanitize(requireNotNull(song)) deviceLibrary.findSong(requireNotNull(song).uid)
}, },
positionMs = savedState.positionMs, positionMs = savedState.positionMs,
repeatMode = savedState.repeatMode), repeatMode = savedState.repeatMode),

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.playlist package org.oxycblt.auxio.music.user
import androidx.room.* import androidx.room.*
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music

View file

@ -16,20 +16,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.playlist package org.oxycblt.auxio.music.user
import android.content.Context import android.content.Context
import org.oxycblt.auxio.music.* 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) : class PlaylistImpl(
Playlist { rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary,
musicSettings: MusicSettings
) : Playlist {
override val uid = rawPlaylist.playlistInfo.playlistUid override val uid = rawPlaylist.playlistInfo.playlistUid
override val rawName = rawPlaylist.playlistInfo.name override val rawName = rawPlaylist.playlistInfo.name
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
override val rawSortName = null override val rawSortName = null
override val sortName = SortName(rawName, musicSettings) 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 durationMs = songs.sumOf { it.durationMs }
override val albums = override val albums =
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.playlist package org.oxycblt.auxio.music.user
import androidx.room.* import androidx.room.*
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music

View file

@ -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]
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * 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/>. * 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 android.content.Context
import androidx.room.Room import androidx.room.Room
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -28,7 +29,13 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @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 fun playlistDao(database: PlaylistDatabase) = database.playlistDao()
@Provides @Provides

View file

@ -24,7 +24,6 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.* 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 * 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) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.library && musicRepository.library != null) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
refreshChoices() refreshChoices()
} }
} }
@ -71,8 +70,7 @@ class PickerViewModel @Inject constructor(private val musicRepository: MusicRepo
* @param uid The [Music.UID] of the [Song] to update to. * @param uid The [Music.UID] of the [Song] to update to.
*/ */
fun setItemUid(uid: Music.UID) { fun setItemUid(uid: Music.UID) {
val library = unlikelyToBeNull(musicRepository.library) _currentItem.value = musicRepository.find(uid)
_currentItem.value = library.find(uid)
refreshChoices() refreshChoices()
} }

View file

@ -282,7 +282,7 @@ constructor(
check(song == null || parent == null || parent.songs.contains(song)) { check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent" "Song to play not in parent"
} }
val library = musicRepository.library ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
val sort = val sort =
when (parent) { when (parent) {
is Genre -> musicSettings.genreSongSort is Genre -> musicSettings.genreSongSort
@ -291,7 +291,7 @@ constructor(
is Playlist -> TODO("handle this") is Playlist -> TODO("handle this")
null -> musicSettings.songSort 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) playbackManager.play(song, parent, queue, shuffled)
} }
@ -469,14 +469,11 @@ constructor(
*/ */
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val library = musicRepository.library val savedState = persistenceRepository.readState()
if (library != null) { if (savedState != null) {
val savedState = persistenceRepository.readState(library) playbackManager.applySavedState(savedState, true)
if (savedState != null) { onDone(true)
playbackManager.applySavedState(savedState, true) return@launch
onDone(true)
return@launch
}
} }
onDone(false) onDone(false)
} }

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.playback.persist
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.MusicParent 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.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -32,12 +32,8 @@ import org.oxycblt.auxio.util.logE
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface PersistenceRepository { interface PersistenceRepository {
/** /** Read the previously persisted [PlaybackStateManager.SavedState]. */
* Read the previously persisted [PlaybackStateManager.SavedState]. suspend fun readState(): PlaybackStateManager.SavedState?
*
* @param library The [Library] required to de-serialize the [PlaybackStateManager.SavedState].
*/
suspend fun readState(library: Library): PlaybackStateManager.SavedState?
/** /**
* Persist a new [PlaybackStateManager.SavedState]. * Persist a new [PlaybackStateManager.SavedState].
@ -49,10 +45,14 @@ interface PersistenceRepository {
class PersistenceRepositoryImpl class PersistenceRepositoryImpl
@Inject @Inject
constructor(private val playbackStateDao: PlaybackStateDao, private val queueDao: QueueDao) : constructor(
PersistenceRepository { 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 playbackState: PlaybackState
val heap: List<QueueHeapItem> val heap: List<QueueHeapItem>
val mapping: List<QueueMappingItem> val mapping: List<QueueMappingItem>
@ -73,14 +73,14 @@ constructor(private val playbackStateDao: PlaybackStateDao, private val queueDao
shuffledMapping.add(entry.shuffledIndex) 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") logD("Read playback state")
return PlaybackStateManager.SavedState( return PlaybackStateManager.SavedState(
parent = parent, parent = parent,
queueState = queueState =
Queue.SavedState( Queue.SavedState(
heap.map { library.find(it.uid) }, heap.map { deviceLibrary.findSong(it.uid) },
orderedMapping, orderedMapping,
shuffledMapping, shuffledMapping,
playbackState.index, playbackState.index,

View file

@ -299,7 +299,7 @@ class PlaybackService :
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { 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. // We now have a library, see if we have anything we need to do.
playbackManager.requestAction(this) playbackManager.requestAction(this)
} }
@ -328,8 +328,8 @@ class PlaybackService :
} }
override fun performAction(action: InternalPlayer.Action): Boolean { override fun performAction(action: InternalPlayer.Action): Boolean {
val library = val deviceLibrary =
musicRepository.library musicRepository.deviceLibrary
// No library, cannot do anything. // No library, cannot do anything.
?: return false ?: return false
@ -339,22 +339,23 @@ class PlaybackService :
// Restore state -> Start a new restoreState job // Restore state -> Start a new restoreState job
is InternalPlayer.Action.RestoreState -> { is InternalPlayer.Action.RestoreState -> {
restoreScope.launch { restoreScope.launch {
persistenceRepository.readState(library)?.let { persistenceRepository.readState()?.let {
playbackManager.applySavedState(it, false) playbackManager.applySavedState(it, false)
} }
} }
} }
// Shuffle all -> Start new playback from all songs // Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> { 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 // Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> { is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song -> deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play( playbackManager.play(
song, song,
null, null,
musicSettings.songSort.songs(library.songs), musicSettings.songSort.songs(deviceLibrary.songs),
playbackManager.queue.isShuffled && playbackSettings.keepShuffle) playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
} }
} }

View file

@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.* 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.playback.PlaybackSettings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -73,7 +73,7 @@ constructor(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.library && musicRepository.library != null) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
search(lastQuery) search(lastQuery)
} }
} }
@ -89,8 +89,8 @@ constructor(
currentSearchJob?.cancel() currentSearchJob?.cancel()
lastQuery = query lastQuery = query
val library = musicRepository.library val deviceLibrary = musicRepository.deviceLibrary
if (query.isNullOrEmpty() || library == null) { if (query.isNullOrEmpty() || deviceLibrary == null) {
logD("Search query is not applicable.") logD("Search query is not applicable.")
_searchResults.value = listOf() _searchResults.value = listOf()
return return
@ -101,23 +101,27 @@ constructor(
// Searching is time-consuming, so do it in the background. // Searching is time-consuming, so do it in the background.
currentSearchJob = currentSearchJob =
viewModelScope.launch { 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 filterMode = searchSettings.searchFilterMode
val items = val items =
if (filterMode == null) { if (filterMode == null) {
// A nulled filter mode means to not filter anything. // 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 { } else {
SearchEngine.Items( SearchEngine.Items(
songs = if (filterMode == MusicMode.SONGS) library.songs else null, songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null,
albums = if (filterMode == MusicMode.ALBUMS) library.albums else null, albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null,
artists = if (filterMode == MusicMode.ARTISTS) library.artists else null, artists = if (filterMode == MusicMode.ARTISTS) deviceLibrary.artists else null,
genres = if (filterMode == MusicMode.GENRES) library.genres else null) genres = if (filterMode == MusicMode.GENRES) deviceLibrary.genres else null)
} }
val results = searchEngine.search(items, query) val results = searchEngine.search(items, query)

View file

@ -143,7 +143,7 @@
tools:layout="@layout/dialog_pre_amp" /> tools:layout="@layout/dialog_pre_amp" />
<dialog <dialog
android:id="@+id/music_dirs_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" android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" /> tools:layout="@layout/dialog_music_dirs" />
<dialog <dialog

View file

@ -20,12 +20,11 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri 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.Date
import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.Disc
import org.oxycblt.auxio.music.metadata.ReleaseType 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 { open class FakeSong : Song {
override val rawName: String? override val rawName: String?

View file

@ -19,24 +19,16 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import kotlinx.coroutines.Job 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 { open class FakeMusicRepository : MusicRepository {
override var indexingState: IndexingState? override val indexingState: IndexingState?
get() = throw NotImplementedError() get() = throw NotImplementedError()
set(_) { override val deviceLibrary: DeviceLibrary?
throw NotImplementedError()
}
override var library: Library?
get() = throw NotImplementedError() get() = throw NotImplementedError()
set(_) { override val userLibrary: UserLibrary?
throw NotImplementedError()
}
override var playlists: List<Playlist>?
get() = throw NotImplementedError() get() = throw NotImplementedError()
set(_) {
throw NotImplementedError()
}
override fun addUpdateListener(listener: MusicRepository.UpdateListener) { override fun addUpdateListener(listener: MusicRepository.UpdateListener) {
throw NotImplementedError() throw NotImplementedError()
@ -62,6 +54,10 @@ open class FakeMusicRepository : MusicRepository {
throw NotImplementedError() throw NotImplementedError()
} }
override fun find(uid: Music.UID): Music? {
throw NotImplementedError()
}
override fun requestIndex(withCache: Boolean) { override fun requestIndex(withCache: Boolean) {
throw NotImplementedError() throw NotImplementedError()
} }

View file

@ -19,7 +19,7 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.storage.MusicDirectories import org.oxycblt.auxio.music.fs.MusicDirectories
open class FakeMusicSettings : MusicSettings { open class FakeMusicSettings : MusicSettings {
override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError() override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError()

View file

@ -21,8 +21,8 @@ package org.oxycblt.auxio.music
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.library.FakeLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.device.FakeDeviceLibrary
import org.oxycblt.auxio.util.forceClear import org.oxycblt.auxio.util.forceClear
class MusicViewModelTest { class MusicViewModelTest {
@ -49,7 +49,7 @@ class MusicViewModelTest {
val musicRepository = TestMusicRepository() val musicRepository = TestMusicRepository()
val musicViewModel = MusicViewModel(musicRepository) val musicViewModel = MusicViewModel(musicRepository)
assertEquals(null, musicViewModel.statistics.value) assertEquals(null, musicViewModel.statistics.value)
musicRepository.library = TestLibrary() musicRepository.deviceLibrary = TestDeviceLibrary()
assertEquals( assertEquals(
MusicViewModel.Statistics( MusicViewModel.Statistics(
2, 2,
@ -71,11 +71,11 @@ class MusicViewModelTest {
} }
private class TestMusicRepository : FakeMusicRepository() { private class TestMusicRepository : FakeMusicRepository() {
override var library: Library? = null override var deviceLibrary: DeviceLibrary? = null
set(value) { set(value) {
field = value field = value
updateListener?.onMusicChanges( updateListener?.onMusicChanges(
MusicRepository.Changes(library = true, playlists = false)) MusicRepository.Changes(deviceLibrary = true, userLibrary = false))
} }
override var indexingState: IndexingState? = null override var indexingState: IndexingState? = null
set(value) { set(value) {
@ -88,7 +88,8 @@ class MusicViewModelTest {
val requests = mutableListOf<Boolean>() val requests = mutableListOf<Boolean>()
override fun addUpdateListener(listener: MusicRepository.UpdateListener) { 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 this.updateListener = listener
} }
@ -110,7 +111,7 @@ class MusicViewModelTest {
} }
} }
private class TestLibrary : FakeLibrary() { private class TestDeviceLibrary : FakeDeviceLibrary() {
override val songs: List<Song> override val songs: List<Song>
get() = listOf(TestSong(), TestSong()) get() = listOf(TestSong(), TestSong())
override val albums: List<Album> override val albums: List<Album>

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * 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/>. * 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.content.Context
import android.net.Uri import android.net.Uri
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
open class FakeLibrary : Library { open class FakeDeviceLibrary : DeviceLibrary {
override val songs: List<Song> override val songs: List<Song>
get() = throw NotImplementedError() get() = throw NotImplementedError()
override val albums: List<Album> override val albums: List<Album>
@ -32,7 +32,7 @@ open class FakeLibrary : Library {
override val genres: List<Genre> override val genres: List<Genre>
get() = throw NotImplementedError() get() = throw NotImplementedError()
override fun <T : Music> find(uid: Music.UID): T? { override fun findSong(uid: Music.UID): Song? {
throw NotImplementedError() throw NotImplementedError()
} }
@ -40,11 +40,15 @@ open class FakeLibrary : Library {
throw NotImplementedError() throw NotImplementedError()
} }
override fun <T : MusicParent> sanitize(parent: T): T? { override fun findAlbum(uid: Music.UID): Album? {
throw NotImplementedError() 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() throw NotImplementedError()
} }
} }

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.library package org.oxycblt.auxio.music.device
import java.util.* import java.util.*
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals