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) {
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()

View file

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

View file

@ -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))
}
/**

View file

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

View file

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

View file

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

View file

@ -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() {}

View file

@ -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() {

View file

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

View file

@ -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.*
/**

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),

View file

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

View file

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

View file

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

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
* 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

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