diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt index 218772315..98dff1eb5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt @@ -121,7 +121,7 @@ private class DetailGeneratorImpl( } override fun album(uid: Music.UID): Detail? { - val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null + val album = musicRepository.library?.findAlbum(uid) ?: return null val songs = listSettings.albumSongSort.songs(album.songs) val discs = songs.groupBy { it.disc } val section = @@ -134,7 +134,7 @@ private class DetailGeneratorImpl( } override fun artist(uid: Music.UID): Detail? { - val artist = musicRepository.deviceLibrary?.findArtist(uid) ?: return null + val artist = musicRepository.library?.findArtist(uid) ?: return null val grouping = artist.explicitAlbums.groupByTo(sortedMapOf()) { // Remap the complicated ReleaseType data structure into detail sections @@ -173,14 +173,14 @@ private class DetailGeneratorImpl( } override fun genre(uid: Music.UID): Detail? { - val genre = musicRepository.deviceLibrary?.findGenre(uid) ?: return null + val genre = musicRepository.library?.findGenre(uid) ?: return null val artists = DetailSection.Artists(GENRE_ARTIST_SORT.artists(genre.artists)) val songs = DetailSection.Songs(listSettings.genreSongSort.songs(genre.songs)) return Detail(genre, listOf(artists, songs)) } override fun playlist(uid: Music.UID): Detail? { - val playlist = musicRepository.userLibrary?.findPlaylist(uid) ?: return null + val playlist = musicRepository.library?.findPlaylist(uid) ?: return null if (playlist.songs.isNotEmpty()) { val songs = DetailSection.Songs(playlist.songs) return Detail(playlist, listOf(songs)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 2f108917b..53b79f18f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -315,7 +315,7 @@ constructor( */ fun setSong(uid: Music.UID) { L.d("Opening song $uid") - _currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) + _currentSong.value = musicRepository.library?.findSong(uid)?.also(::refreshAudioInfo) if (_currentSong.value == null) { L.w("Given song UID was invalid") } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt index 50a8d33e4..15c7dfe35 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt @@ -25,10 +25,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.model.DeviceLibrary import timber.log.Timber as L /** @@ -56,9 +56,9 @@ class DetailPickerViewModel @Inject constructor(private val musicRepository: Mus override fun onMusicChanges(changes: MusicRepository.Changes) { if (!changes.deviceLibrary) return - val deviceLibrary = musicRepository.deviceLibrary ?: return + val library = musicRepository.library ?: return // Need to sanitize different items depending on the current set of choices. - _artistChoices.value = _artistChoices.value?.sanitize(deviceLibrary) + _artistChoices.value = _artistChoices.value?.sanitize(library) L.d("Updated artist choices: ${_artistChoices.value}") } @@ -99,15 +99,14 @@ sealed interface ArtistShowChoices { /** The current [Artist] choices. */ val choices: List /** Sanitize this instance with a [DeviceLibrary]. */ - fun sanitize(newLibrary: DeviceLibrary): ArtistShowChoices? + fun sanitize(newLibrary: Library): ArtistShowChoices? /** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */ class FromSong(val song: Song) : ArtistShowChoices { override val uid = song.uid override val choices = song.artists - override fun sanitize(newLibrary: DeviceLibrary) = - newLibrary.findSong(uid)?.let { FromSong(it) } + override fun sanitize(newLibrary: Library) = newLibrary.findSong(uid)?.let { FromSong(it) } } /** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */ @@ -115,7 +114,7 @@ sealed interface ArtistShowChoices { override val uid = album.uid override val choices = album.artists - override fun sanitize(newLibrary: DeviceLibrary) = + override fun sanitize(newLibrary: Library) = newLibrary.findAlbum(uid)?.let { FromAlbum(it) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt index 19f24c27d..50f8e29e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt @@ -119,8 +119,8 @@ private class HomeGeneratorImpl( } override fun onMusicChanges(changes: MusicRepository.Changes) { - val deviceLibrary = musicRepository.deviceLibrary - if (changes.deviceLibrary && deviceLibrary != null) { + val library = musicRepository.library + if (changes.deviceLibrary && library != null) { L.d("Refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. @@ -130,8 +130,7 @@ private class HomeGeneratorImpl( invalidator.invalidateMusic(MusicType.GENRES, UpdateInstructions.Diff) } - val userLibrary = musicRepository.userLibrary - if (changes.userLibrary && userLibrary != null) { + if (changes.userLibrary && library != null) { L.d("Refreshing playlists") invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff) } @@ -144,14 +143,13 @@ private class HomeGeneratorImpl( } override fun songs() = - musicRepository.deviceLibrary?.let { listSettings.songSort.songs(it.songs) } ?: emptyList() + musicRepository.library?.let { listSettings.songSort.songs(it.songs) } ?: emptyList() override fun albums() = - musicRepository.deviceLibrary?.let { listSettings.albumSort.albums(it.albums) } - ?: emptyList() + musicRepository.library?.let { listSettings.albumSort.albums(it.albums) } ?: emptyList() override fun artists() = - musicRepository.deviceLibrary?.let { deviceLibrary -> + musicRepository.library?.let { deviceLibrary -> val sorted = listSettings.artistSort.artists(deviceLibrary.artists) if (homeSettings.shouldHideCollaborators) { sorted.filter { it.explicitAlbums.isNotEmpty() } @@ -161,11 +159,10 @@ private class HomeGeneratorImpl( } ?: emptyList() override fun genres() = - musicRepository.deviceLibrary?.let { listSettings.genreSort.genres(it.genres) } - ?: emptyList() + musicRepository.library?.let { listSettings.genreSort.genres(it.genres) } ?: emptyList() override fun playlists() = - musicRepository.userLibrary?.let { listSettings.playlistSort.playlists(it.playlists) } + musicRepository.library?.let { listSettings.playlistSort.playlists(it.playlists) } ?: emptyList() override fun tabs() = homeSettings.homeTabs.filterIsInstance().map { it.type } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt index 0de81e3c1..3d35d77bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt @@ -64,18 +64,17 @@ constructor(private val listSettings: ListSettings, private val musicRepository: } override fun onMusicChanges(changes: MusicRepository.Changes) { - val deviceLibrary = musicRepository.deviceLibrary ?: return - val userLibrary = musicRepository.userLibrary ?: return + val library = musicRepository.library ?: 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 -> 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) + is Song -> library.findSong(it.uid) + is Album -> library.findAlbum(it.uid) + is Artist -> library.findArtist(it.uid) + is Genre -> library.findGenre(it.uid) + is Playlist -> library.findPlaylist(it.uid) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt index 387f8dc80..5b56bd9d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt @@ -70,35 +70,35 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi } private fun unpackSongParcel(parcel: Menu.ForSong.Parcel): Menu.ForSong? { - val song = musicRepository.deviceLibrary?.findSong(parcel.songUid) ?: return null + val song = musicRepository.library?.findSong(parcel.songUid) ?: return null val parent = parcel.playWithUid?.let(musicRepository::find) as MusicParent? val playWith = PlaySong.fromIntCode(parcel.playWithCode, parent) ?: return null return Menu.ForSong(parcel.res, song, playWith) } private fun unpackAlbumParcel(parcel: Menu.ForAlbum.Parcel): Menu.ForAlbum? { - val album = musicRepository.deviceLibrary?.findAlbum(parcel.albumUid) ?: return null + val album = musicRepository.library?.findAlbum(parcel.albumUid) ?: return null return Menu.ForAlbum(parcel.res, album) } private fun unpackArtistParcel(parcel: Menu.ForArtist.Parcel): Menu.ForArtist? { - val artist = musicRepository.deviceLibrary?.findArtist(parcel.artistUid) ?: return null + val artist = musicRepository.library?.findArtist(parcel.artistUid) ?: return null return Menu.ForArtist(parcel.res, artist) } private fun unpackGenreParcel(parcel: Menu.ForGenre.Parcel): Menu.ForGenre? { - val genre = musicRepository.deviceLibrary?.findGenre(parcel.genreUid) ?: return null + val genre = musicRepository.library?.findGenre(parcel.genreUid) ?: return null return Menu.ForGenre(parcel.res, genre) } private fun unpackPlaylistParcel(parcel: Menu.ForPlaylist.Parcel): Menu.ForPlaylist? { - val playlist = musicRepository.userLibrary?.findPlaylist(parcel.playlistUid) ?: return null + val playlist = musicRepository.library?.findPlaylist(parcel.playlistUid) ?: return null return Menu.ForPlaylist(parcel.res, playlist) } private fun unpackSelectionParcel(parcel: Menu.ForSelection.Parcel): Menu.ForSelection? { - val deviceLibrary = musicRepository.deviceLibrary ?: return null - val songs = parcel.songUids.mapNotNull(deviceLibrary::findSong) + val library = musicRepository.library ?: return null + val songs = parcel.songUids.mapNotNull(library::findSong) return Menu.ForSelection(parcel.res, songs) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index c1431a3f0..0e45a5e6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -40,6 +40,28 @@ import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull +interface Library { + val songs: Collection + val albums: Collection + val artists: Collection + val genres: Collection + val playlists: Collection + + fun findSong(uid: Music.UID): Song? + + fun findSongByPath(path: Path): Song? + + fun findAlbum(uid: Music.UID): Album? + + fun findArtist(uid: Music.UID): Artist? + + fun findGenre(uid: Music.UID): Genre? + + fun findPlaylist(uid: Music.UID): Playlist? + + fun findPlaylistByName(name: String): Playlist? +} + /** * Abstract music data. This contains universal information about all concrete music * implementations, such as identification information and names. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 77a6c4ead..c03cc883d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -29,12 +29,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.stack.Indexer -import org.oxycblt.auxio.music.user.MutableUserLibrary -import org.oxycblt.auxio.music.user.UserLibrary +import org.oxycblt.auxio.music.stack.interpret.Interpretation +import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary import timber.log.Timber as L /** @@ -49,10 +48,7 @@ import timber.log.Timber as L * configurations */ interface MusicRepository { - /** The current music information found on the device. */ - val deviceLibrary: DeviceLibrary? - /** The current user-defined music information. */ - val userLibrary: UserLibrary? + val library: Library? /** The current state of music loading. Null if no load has occurred yet. */ val indexingState: IndexingState? @@ -182,7 +178,7 @@ interface MusicRepository { * Flags indicating which kinds of music information changed. * * @param deviceLibrary Whether the current [DeviceLibrary] has changed. - * @param userLibrary Whether the current [Playlist]s have changed. + * @param library Whether the current [Playlist]s have changed. */ data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean) @@ -212,18 +208,13 @@ interface MusicRepository { class MusicRepositoryImpl @Inject -constructor( - private val indexer: Indexer, - private val deviceLibraryFactory: DeviceLibrary.Factory, - private val userLibraryFactory: UserLibrary.Factory, - private val musicSettings: MusicSettings -) : MusicRepository { +constructor(private val indexer: Indexer, private val musicSettings: MusicSettings) : + MusicRepository { private val updateListeners = mutableListOf() private val indexingListeners = mutableListOf() @Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null - @Volatile override var deviceLibrary: DeviceLibrary? = null - @Volatile override var userLibrary: MutableUserLibrary? = null + @Volatile override var library: MutableLibrary? = null @Volatile private var previousCompletedState: IndexingState.Completed? = null @Volatile private var currentIndexingState: IndexingState? = null override val indexingState: IndexingState? @@ -282,41 +273,50 @@ constructor( @Synchronized override fun find(uid: Music.UID) = - (deviceLibrary?.run { findSong(uid) ?: findAlbum(uid) ?: findArtist(uid) ?: findGenre(uid) } - ?: userLibrary?.findPlaylist(uid)) + (library?.run { + findSong(uid) + ?: findAlbum(uid) + ?: findArtist(uid) + ?: findGenre(uid) + ?: findPlaylist(uid) + }) override suspend fun createPlaylist(name: String, songs: List) { - val userLibrary = synchronized(this) { userLibrary ?: return } + val library = synchronized(this) { library ?: return } L.d("Creating playlist $name with ${songs.size} songs") - userLibrary.createPlaylist(name, songs) + val newLibrary = library.createPlaylist(name, songs) + synchronized(this) { this.library = newLibrary } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun renamePlaylist(playlist: Playlist, name: String) { - val userLibrary = synchronized(this) { userLibrary ?: return } + val library = synchronized(this) { library ?: return } L.d("Renaming $playlist to $name") - userLibrary.renamePlaylist(playlist, name) + val newLibrary = library.renamePlaylist(playlist, name) + synchronized(this) { this.library = newLibrary } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun deletePlaylist(playlist: Playlist) { - val userLibrary = synchronized(this) { userLibrary ?: return } + val library = synchronized(this) { library ?: return } L.d("Deleting $playlist") - userLibrary.deletePlaylist(playlist) + val newLibrary = library.deletePlaylist(playlist) + synchronized(this) { this.library = newLibrary } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { - val userLibrary = synchronized(this) { userLibrary ?: return } + val library = synchronized(this) { library ?: return } L.d("Adding ${songs.size} songs to $playlist") - userLibrary.addToPlaylist(playlist, songs) + val newLibrary = library.addToPlaylist(playlist, songs) + synchronized(this) { this.library = newLibrary } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { - val userLibrary = synchronized(this) { userLibrary ?: return } + val library = synchronized(this) { library ?: return } L.d("Rewriting $playlist with ${songs.size} songs") - userLibrary.rewritePlaylist(playlist, songs) + library.rewritePlaylist(playlist, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } @@ -363,25 +363,28 @@ constructor( Name.Known.SimpleFactory } - val (deviceLibrary, userLibrary) = indexer.run(listOf(), separators, nameFactory) + val newLibrary = indexer.run(listOf(), Interpretation(nameFactory, separators)) - val deviceLibraryChanged: Boolean - val userLibraryChanged: Boolean // We want to make sure that all reads and writes are synchronized due to the sheer // amount of consumers of MusicRepository. // TODO: Would Atomics not be a better fit here? + val deviceLibraryChanged: Boolean + val userLibraryChanged: Boolean synchronized(this) { // It's possible that this reload might have changed nothing, so make sure that // hasn't happened before dispatching a change to all consumers. - deviceLibraryChanged = this.deviceLibrary != deviceLibrary - userLibraryChanged = this.userLibrary != userLibrary + deviceLibraryChanged = + this.library?.songs != newLibrary.songs || + this.library?.albums != newLibrary.albums || + this.library?.artists != newLibrary.artists || + this.library?.genres != newLibrary.genres + userLibraryChanged = this.library?.playlists != newLibrary.playlists if (!deviceLibraryChanged && !userLibraryChanged) { L.d("Library has not changed, skipping update") return } - this.deviceLibrary = deviceLibrary - this.userLibrary = userLibrary + this.library = newLibrary } // Consumers expect their updates to be on the main thread (notably PlaybackService), diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 63f56fff7..177c1151e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -85,14 +85,14 @@ constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { if (!changes.deviceLibrary) return - val deviceLibrary = musicRepository.deviceLibrary ?: return + val library = musicRepository.library ?: return _statistics.value = Statistics( - deviceLibrary.songs.size, - deviceLibrary.albums.size, - deviceLibrary.artists.size, - deviceLibrary.genres.size, - deviceLibrary.songs.sumOf { it.durationMs }) + library.songs.size, + library.albums.size, + library.artists.size, + library.genres.size, + library.songs.sumOf { it.durationMs }) L.d("Updated statistics: ${_statistics.value}") } @@ -162,10 +162,10 @@ constructor( return@launch } - val deviceLibrary = musicRepository.deviceLibrary ?: return@launch + val library = musicRepository.library ?: return@launch val songs = importedPlaylist.paths.mapNotNull { - it.firstNotNullOfOrNull(deviceLibrary::findSongByPath) + it.firstNotNullOfOrNull(library::findSongByPath) } if (songs.isEmpty()) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt index 2bc676bdf..be6ef5c00 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt @@ -89,13 +89,13 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M override fun onMusicChanges(changes: MusicRepository.Changes) { var refreshChoicesWith: List? = null - val deviceLibrary = musicRepository.deviceLibrary - if (changes.deviceLibrary && deviceLibrary != null) { + val library = musicRepository.library + if (changes.deviceLibrary && library != null) { _currentPendingNewPlaylist.value = _currentPendingNewPlaylist.value?.let { pendingPlaylist -> PendingNewPlaylist( pendingPlaylist.preferredName, - pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) }, + pendingPlaylist.songs.mapNotNull { library.findSong(it.uid) }, pendingPlaylist.template, pendingPlaylist.reason) } @@ -104,7 +104,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M _currentSongsToAdd.value = _currentSongsToAdd.value?.let { pendingSongs -> pendingSongs - .mapNotNull { deviceLibrary.findSong(it.uid) } + .mapNotNull { library.findSong(it.uid) } .ifEmpty { null } .also { refreshChoicesWith = it } } @@ -127,7 +127,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M _currentPlaylistToExport.value = _currentPlaylistToExport.value?.let { playlist -> - musicRepository.userLibrary?.findPlaylist(playlist.uid) + musicRepository.library?.findPlaylist(playlist.uid) } L.d("Updated playlist to export to ${_currentPlaylistToExport.value}") } @@ -153,14 +153,14 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M reason: PlaylistDecision.New.Reason ) { L.d("Opening ${songUids.size} songs to create a playlist from") - val userLibrary = musicRepository.userLibrary ?: return + val library = musicRepository.library ?: return val songs = - musicRepository.deviceLibrary + musicRepository.library ?.let { songUids.mapNotNull(it::findSong) } ?.also(::refreshPlaylistChoices) val possibleName = - musicRepository.userLibrary?.let { + musicRepository.library?.let { // Attempt to generate a unique default name for the playlist, like "Playlist 1". var i = 1 var possibleName: String @@ -168,7 +168,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M possibleName = context.getString(R.string.fmt_def_playlist, i) L.d("Trying $possibleName as a playlist name") ++i - } while (userLibrary.playlists.any { it.name.resolve(context) == possibleName }) + } while (library.playlists.any { it.name.resolve(context) == possibleName }) L.d("$possibleName is unique, using it as the playlist name") possibleName } @@ -194,9 +194,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M reason: PlaylistDecision.Rename.Reason ) { L.d("Opening playlist $playlistUid to rename") - val playlist = musicRepository.userLibrary?.findPlaylist(playlistUid) - val applySongs = - musicRepository.deviceLibrary?.let { applySongUids.mapNotNull(it::findSong) } + val playlist = musicRepository.library?.findPlaylist(playlistUid) + val applySongs = musicRepository.library?.let { applySongUids.mapNotNull(it::findSong) } _currentPendingRenamePlaylist.value = if (playlist != null && applySongs != null) { @@ -216,7 +215,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M L.d("Opening playlist $playlistUid to export") // TODO: Add this guard to the rest of the methods here if (_currentPlaylistToExport.value?.uid == playlistUid) return - _currentPlaylistToExport.value = musicRepository.userLibrary?.findPlaylist(playlistUid) + _currentPlaylistToExport.value = musicRepository.library?.findPlaylist(playlistUid) if (_currentPlaylistToExport.value == null) { L.w("Given playlist UID to export was invalid") } else { @@ -241,7 +240,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M */ fun setPlaylistToDelete(playlistUid: Music.UID) { L.d("Opening playlist $playlistUid to delete") - _currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) + _currentPlaylistToDelete.value = musicRepository.library?.findPlaylist(playlistUid) if (_currentPlaylistToDelete.value == null) { L.w("Given playlist UID to delete was invalid") } @@ -266,8 +265,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M } else -> { val trimmed = name.trim() - val userLibrary = musicRepository.userLibrary - if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { + val library = musicRepository.library + if (library != null && library.findPlaylistByName(trimmed) == null) { L.d("Chosen name is valid") ChosenName.Valid(trimmed) } else { @@ -286,7 +285,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M fun setSongsToAdd(songUids: Array) { L.d("Opening ${songUids.size} songs to add to a playlist") _currentSongsToAdd.value = - musicRepository.deviceLibrary + musicRepository.library ?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } } ?.also(::refreshPlaylistChoices) if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) { @@ -295,10 +294,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M } private fun refreshPlaylistChoices(songs: List) { - val userLibrary = musicRepository.userLibrary ?: return + val library = musicRepository.library ?: return L.d("Refreshing playlist choices") _playlistAddChoices.value = - Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map { + Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(library.playlists).map { val songSet = it.songs.toSet() PlaylistChoice(it, songs.all(songSet::contains)) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 871852a9f..6f8ad1c86 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -151,8 +151,7 @@ constructor( else -> listOf( InterpretedPath(Components.parseUnix(path), false), - InterpretedPath(Components.parseWindows(path), true) - ) + InterpretedPath(Components.parseWindows(path), true)) } private fun expandInterpretation( diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index eaa38208e..289e4e59f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Auxio Prct + * Copyright (c) 2023 Auxio Project * Name.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index 121fb38c5..e6a975d1c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. return AudioProperties( bitrate, sampleRate, - MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType) - ) + MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexingHolder.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexingHolder.kt index 1e319a9ed..50483e7d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexingHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexingHolder.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * Indexer.kt is part of Auxio. + * IndexingHolder.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 @@ -146,7 +146,7 @@ private constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - val deviceLibrary = musicRepository.deviceLibrary ?: return + val library = musicRepository.library ?: return L.d("Music changed, updating shared objects") // Wipe possibly-invalidated outdated covers imageLoader.memoryCache?.clear() @@ -158,10 +158,7 @@ private constructor( savedState.copy( parent = savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent? }, - heap = - savedState.heap.map { song -> - song?.let { deviceLibrary.findSong(it.uid) } - }), + heap = savedState.heap.map { song -> song?.let { library.findSong(it.uid) } }), true) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt index 26751d587..3013752c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt @@ -95,14 +95,13 @@ private constructor( } override fun invalidate(type: MusicType, replace: Int?) { - val deviceLibrary = musicRepository.deviceLibrary ?: return - val userLibrary = musicRepository.userLibrary ?: return + val library = musicRepository.library ?: return val music = when (type) { - MusicType.ALBUMS -> deviceLibrary.albums - MusicType.ARTISTS -> deviceLibrary.artists - MusicType.GENRES -> deviceLibrary.genres - MusicType.PLAYLISTS -> userLibrary.playlists + MusicType.ALBUMS -> library.albums + MusicType.ARTISTS -> library.artists + MusicType.GENRES -> library.genres + MusicType.PLAYLISTS -> library.playlists else -> return } if (music.isEmpty()) { @@ -131,9 +130,7 @@ private constructor( } fun getChildren(parentId: String, maxTabs: Int): List? { - val deviceLibrary = musicRepository.deviceLibrary - val userLibrary = musicRepository.userLibrary - if (deviceLibrary == null || userLibrary == null) { + if (musicRepository.library == null) { return listOf() } return getMediaItemList(parentId, maxTabs) @@ -143,15 +140,10 @@ private constructor( if (query.isEmpty()) { return mutableListOf() } - val deviceLibrary = musicRepository.deviceLibrary ?: return mutableListOf() - val userLibrary = musicRepository.userLibrary ?: return mutableListOf() + val library = musicRepository.library ?: return mutableListOf() val items = SearchEngine.Items( - deviceLibrary.songs, - deviceLibrary.albums, - deviceLibrary.artists, - deviceLibrary.genres, - userLibrary.playlists) + library.songs, library.albums, library.artists, library.genres, library.playlists) return searchEngine.search(items, query).toMediaItems() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt index 181fda3a1..c7d55e721 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt @@ -23,7 +23,6 @@ import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOn import org.oxycblt.auxio.music.stack.explore.Explorer import org.oxycblt.auxio.music.stack.interpret.Interpretation @@ -31,23 +30,13 @@ import org.oxycblt.auxio.music.stack.interpret.Interpreter import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary interface Indexer { - suspend fun run( - uris: List, - interpretation: Interpretation - ): MutableLibrary + suspend fun run(uris: List, interpretation: Interpretation): MutableLibrary } - class IndexerImpl @Inject -constructor( - private val explorer: Explorer, - private val interpreter: Interpreter -) : Indexer { - override suspend fun run( - uris: List, - interpretation: Interpretation - ) = coroutineScope { +constructor(private val explorer: Explorer, private val interpreter: Interpreter) : Indexer { + override suspend fun run(uris: List, interpretation: Interpretation) = coroutineScope { val files = explorer.explore(uris) val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer() val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer() diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/ExploreModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/ExploreModule.kt index 96ed54ee3..6c7e80138 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/ExploreModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/ExploreModule.kt @@ -1,11 +1,27 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExploreModule.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.explore import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.music.stack.Indexer -import org.oxycblt.auxio.music.stack.IndexerImpl @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Explorer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Explorer.kt index 3631ea1a0..22e7d37e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Explorer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Explorer.kt @@ -1,41 +1,55 @@ +/* + * Copyright (c) 2024 Auxio Project + * Explorer.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.explore import android.net.Uri +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.withIndex +import org.oxycblt.auxio.music.stack.explore.cache.CacheResult import org.oxycblt.auxio.music.stack.explore.cache.TagCache import org.oxycblt.auxio.music.stack.explore.extractor.TagExtractor -import org.oxycblt.auxio.music.stack.explore.cache.CacheResult import org.oxycblt.auxio.music.stack.explore.fs.DeviceFiles import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists -import javax.inject.Inject interface Explorer { fun explore(uris: List): Files } -data class Files( - val audios: Flow, - val playlists: Flow -) +data class Files(val audios: Flow, val playlists: Flow) -class ExplorerImpl @Inject constructor( +class ExplorerImpl +@Inject +constructor( private val deviceFiles: DeviceFiles, private val tagCache: TagCache, private val tagExtractor: TagExtractor, @@ -46,16 +60,20 @@ class ExplorerImpl @Inject constructor( val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer() val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer() val (uncachedDeviceFiles, cachedAudioFiles) = tagRead.results() - val extractedAudioFiles = uncachedDeviceFiles.split(8).map { - tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() - }.asFlow().flattenMerge() + val extractedAudioFiles = + uncachedDeviceFiles + .split(8) + .map { tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() } + .asFlow() + .flattenMerge() val writtenAudioFiles = tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer() val playlistFiles = storedPlaylists.read() return Files(merge(cachedAudioFiles, writtenAudioFiles), playlistFiles) } private fun Flow.results(): Pair, Flow> { - val shared = shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) + val shared = + shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) val files = shared.filterIsInstance().map { it.deviceFile } val songs = shared.filterIsInstance().map { it.audioFile } return files to songs @@ -63,10 +81,9 @@ class ExplorerImpl @Inject constructor( private fun Flow.split(n: Int): Array> { val indexed = withIndex() - val shared = indexed.shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) - return Array(n) { - shared.filter { it.index % n == 0 } - .map { it.value } - } + val shared = + indexed.shareIn( + CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) + return Array(n) { shared.filter { it.index % n == 0 }.map { it.value } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Files.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Files.kt index 61db99837..4861a8126 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Files.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/Files.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * Preparer.kt is part of Auxio. + * Files.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 @@ -21,9 +21,9 @@ package org.oxycblt.auxio.music.stack.explore import android.net.Uri import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.stack.explore.fs.Path +import org.oxycblt.auxio.music.stack.interpret.model.SongImpl data class DeviceFile( val uri: Uri, @@ -71,13 +71,17 @@ data class PlaylistFile( interface PlaylistHandle { val uid: Music.UID + suspend fun rename(name: String) + suspend fun add(songs: List) + suspend fun rewrite(songs: List) + suspend fun delete() } sealed interface SongPointer { data class UID(val uid: Music.UID) : SongPointer -// data class Path(val options: List) : SongPointer + // data class Path(val options: List) : SongPointer } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagCache.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagCache.kt index 3664b4d98..42a7c8fa6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagCache.kt @@ -27,8 +27,10 @@ import org.oxycblt.auxio.music.stack.explore.DeviceFile sealed interface CacheResult { data class Hit(val audioFile: AudioFile) : CacheResult + data class Miss(val deviceFile: DeviceFile) : CacheResult } + interface TagCache { fun read(files: Flow): Flow diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagDatabase.kt index 3ce0e3181..9c6d117e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/cache/TagDatabase.kt @@ -28,8 +28,8 @@ import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters -import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.DeviceFile import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped @@ -51,8 +51,8 @@ interface TagDao { @TypeConverters(Tags.Converters::class) data class Tags( /** - * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black box - * only used for comparison. + * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black + * box only used for comparison. */ @PrimaryKey val uri: String, /** The latest date the [AudioFile]'s audio file was modified, as a unix epoch timestamp. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt index e2da48e2f..e2875e8f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.stack.explore.extractor import android.content.Context @@ -26,33 +26,29 @@ import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.guava.asDeferred -import javax.inject.Inject import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.DeviceFile - interface TagExtractor { - fun extract( - deviceFiles: Flow - ): Flow + fun extract(deviceFiles: Flow): Flow } -class TagExtractorImpl @Inject constructor( +class TagExtractorImpl +@Inject +constructor( @ApplicationContext private val context: Context, private val mediaSourceFactory: MediaSource.Factory, ) : TagExtractor { - override fun extract( - deviceFiles: Flow - ) = flow { + override fun extract(deviceFiles: Flow) = flow { val thread = HandlerThread("TagExtractor:${hashCode()}") deviceFiles.collect { deviceFile -> val exoPlayerMetadataFuture = MetadataRetriever.retrieveMetadata( - mediaSourceFactory, MediaItem.fromUri(deviceFile.uri), thread - ) + mediaSourceFactory, MediaItem.fromUri(deviceFile.uri), thread) val mediaMetadataRetriever = MediaMetadataRetriever() mediaMetadataRetriever.setDataSource(context, deviceFile.uri) val exoPlayerMetadata = exoPlayerMetadataFuture.asDeferred().await() @@ -75,9 +71,12 @@ class TagExtractorImpl @Inject constructor( val textTags = TextTags(metadata) return AudioFile( deviceFile = input, - durationMs = need(retriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_DURATION - )?.toLong(), "duration"), + durationMs = + need( + retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toLong(), + "duration"), replayGainTrackAdjustment = textTags.replayGainTrackAdjustment(), replayGainAlbumAdjustment = textTags.replayGainAlbumAdjustment(), musicBrainzId = textTags.musicBrainzId(), @@ -97,15 +96,22 @@ class TagExtractorImpl @Inject constructor( albumArtistMusicBrainzIds = textTags.albumArtistMusicBrainzIds() ?: listOf(), albumArtistNames = textTags.albumArtistNames() ?: listOf(), albumArtistSortNames = textTags.albumArtistSortNames() ?: listOf(), - genreNames = textTags.genreNames() ?: listOf() - ) + genreNames = textTags.genreNames() ?: listOf()) } - private fun defaultAudioFile(deviceFile: DeviceFile, metadataRetriever: MediaMetadataRetriever) = + private fun defaultAudioFile( + deviceFile: DeviceFile, + metadataRetriever: MediaMetadataRetriever + ) = AudioFile( deviceFile, name = need(deviceFile.path.name, "name"), - durationMs = need(metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong(), "duration"), + durationMs = + need( + metadataRetriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toLong(), + "duration"), ) private fun need(a: T, called: String) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagFields.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagFields.kt index 580225019..005bc780d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagFields.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagFields.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagFields.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.explore.extractor import androidx.core.text.isDigitsOnly @@ -6,28 +24,31 @@ import org.oxycblt.auxio.util.nonZeroOrNull // Song fun TextTags.musicBrainzId() = - (vorbis["musicbrainz_releasetrackid"] ?: vorbis["musicbrainz release track id"] - ?: id3v2["TXXX:musicbrainz release track id"] - ?: id3v2["TXXX:musicbrainz_releasetrackid"])?.first() + (vorbis["musicbrainz_releasetrackid"] + ?: vorbis["musicbrainz release track id"] + ?: id3v2["TXXX:musicbrainz release track id"] + ?: id3v2["TXXX:musicbrainz_releasetrackid"]) + ?.first() fun TextTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first() + fun TextTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first() // Track. -fun TextTags.track() = (parseVorbisPositionField( - vorbis["tracknumber"]?.first(), - (vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first() -) - ?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() }) +fun TextTags.track() = + (parseVorbisPositionField( + vorbis["tracknumber"]?.first(), + (vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first()) + ?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() }) // Disc and it's subtitle name. -fun TextTags.disc() = (parseVorbisPositionField( - vorbis["discnumber"]?.first(), - (vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() }) - ?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() }) +fun TextTags.disc() = + (parseVorbisPositionField( + vorbis["discnumber"]?.first(), + (vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() }) + ?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() }) -fun TextTags.subtitle() = - (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first() +fun TextTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first() // Dates are somewhat complicated, as not only did their semantics change from a flat year // value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of @@ -41,111 +62,129 @@ fun TextTags.subtitle() = // TODO: Show original and normal dates side-by-side // TODO: Handle dates that are in "January" because the actual specific release date // isn't known? -fun TextTags.date() = (vorbis["originaldate"]?.run { Date.from(first()) } - ?: vorbis["date"]?.run { Date.from(first()) } - ?: vorbis["year"]?.run { Date.from(first()) } ?: +fun TextTags.date() = + (vorbis["originaldate"]?.run { Date.from(first()) } + ?: vorbis["date"]?.run { Date.from(first()) } + ?: vorbis["year"]?.run { Date.from(first()) } + ?: - // Vorbis dates are less complicated, but there are still several types - // Our hierarchy for dates is as such: - // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue - // 2. Date, as it is the most common date type - // 3. Year, as old vorbis tags tended to use this (I know this because it's the only - // date tag that android supports, so it must be 15 years old or more!) - id3v2["TDOR"]?.run { Date.from(first()) } - ?: id3v2["TDRC"]?.run { Date.from(first()) } - ?: id3v2["TDRL"]?.run { Date.from(first()) } - ?: parseId3v23Date()) + // Vorbis dates are less complicated, but there are still several types + // Our hierarchy for dates is as such: + // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue + // 2. Date, as it is the most common date type + // 3. Year, as old vorbis tags tended to use this (I know this because it's the only + // date tag that android supports, so it must be 15 years old or more!) + id3v2["TDOR"]?.run { Date.from(first()) } + ?: id3v2["TDRC"]?.run { Date.from(first()) } + ?: id3v2["TDRL"]?.run { Date.from(first()) } + ?: parseId3v23Date()) // Album fun TextTags.albumMusicBrainzId() = - (vorbis["musicbrainz_albumid"] ?: vorbis["musicbrainz album id"] - ?: id3v2["TXXX:musicbrainz album id"] ?: id3v2["TXXX:musicbrainz_albumid"])?.first() + (vorbis["musicbrainz_albumid"] + ?: vorbis["musicbrainz album id"] + ?: id3v2["TXXX:musicbrainz album id"] + ?: id3v2["TXXX:musicbrainz_albumid"]) + ?.first() fun TextTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first() + fun TextTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first() -fun TextTags.releaseTypes() = ( - vorbis["releasetype"] ?: vorbis["musicbrainz album type"] + +fun TextTags.releaseTypes() = + (vorbis["releasetype"] + ?: vorbis["musicbrainz album type"] ?: id3v2["TXXX:musicbrainz album type"] ?: id3v2["TXXX:releasetype"] ?: // This is a non-standard iTunes extension - id3v2["GRP1"] - ) + id3v2["GRP1"]) // Artist fun TextTags.artistMusicBrainzIds() = - (vorbis["musicbrainz_artistid"] ?: vorbis["musicbrainz artist id"] - ?: id3v2["TXXX:musicbrainz artist id"] ?: id3v2["TXXX:musicbrainz_artistid"]) + (vorbis["musicbrainz_artistid"] + ?: vorbis["musicbrainz artist id"] + ?: id3v2["TXXX:musicbrainz artist id"] + ?: id3v2["TXXX:musicbrainz_artistid"]) -fun TextTags.artistNames() = (vorbis["artists"] ?: vorbis["artist"] ?: id3v2["TXXX:artists"] -?: id3v2["TPE1"] ?: id3v2["TXXX:artist"]) +fun TextTags.artistNames() = + (vorbis["artists"] + ?: vorbis["artist"] + ?: id3v2["TXXX:artists"] + ?: id3v2["TPE1"] + ?: id3v2["TXXX:artist"]) -fun TextTags.artistSortNames() = (vorbis["artistssort"] - ?: vorbis["artists_sort"] - ?: vorbis["artists sort"] - ?: vorbis["artistsort"] - ?: vorbis["artist sort"] ?: id3v2["TXXX:artistssort"] - ?: id3v2["TXXX:artists_sort"] - ?: id3v2["TXXX:artists sort"] - ?: id3v2["TSOP"] - ?: id3v2["artistsort"] - ?: id3v2["TXXX:artist sort"] - ) +fun TextTags.artistSortNames() = + (vorbis["artistssort"] + ?: vorbis["artists_sort"] + ?: vorbis["artists sort"] + ?: vorbis["artistsort"] + ?: vorbis["artist sort"] + ?: id3v2["TXXX:artistssort"] + ?: id3v2["TXXX:artists_sort"] + ?: id3v2["TXXX:artists sort"] + ?: id3v2["TSOP"] + ?: id3v2["artistsort"] + ?: id3v2["TXXX:artist sort"]) -fun TextTags.albumArtistMusicBrainzIds() = ( - vorbis["musicbrainz_albumartistid"] ?: vorbis["musicbrainz album artist id"] +fun TextTags.albumArtistMusicBrainzIds() = + (vorbis["musicbrainz_albumartistid"] + ?: vorbis["musicbrainz album artist id"] ?: id3v2["TXXX:musicbrainz album artist id"] - ?: id3v2["TXXX:musicbrainz_albumartistid"] - ) + ?: id3v2["TXXX:musicbrainz_albumartistid"]) -fun TextTags.albumArtistNames() = ( - vorbis["albumartists"] - ?: vorbis["album_artists"] - ?: vorbis["album artists"] - ?: vorbis["albumartist"] - ?: vorbis["album artist"] - ?: id3v2["TXXX:albumartists"] - ?: id3v2["TXXX:album_artists"] - ?: id3v2["TXXX:album artists"] - ?: id3v2["TPE2"] - ?: id3v2["TXXX:albumartist"] - ?: id3v2["TXXX:album artist"] - ) +fun TextTags.albumArtistNames() = + (vorbis["albumartists"] + ?: vorbis["album_artists"] + ?: vorbis["album artists"] + ?: vorbis["albumartist"] + ?: vorbis["album artist"] + ?: id3v2["TXXX:albumartists"] + ?: id3v2["TXXX:album_artists"] + ?: id3v2["TXXX:album artists"] + ?: id3v2["TPE2"] + ?: id3v2["TXXX:albumartist"] + ?: id3v2["TXXX:album artist"]) -fun TextTags.albumArtistSortNames() = (vorbis["albumartistssort"] - ?: vorbis["albumartists_sort"] - ?: vorbis["albumartists sort"] - ?: vorbis["albumartistsort"] - ?: vorbis["album artist sort"] ?: id3v2["TXXX:albumartistssort"] - ?: id3v2["TXXX:albumartists_sort"] - ?: id3v2["TXXX:albumartists sort"] - ?: id3v2["TXXX:albumartistsort"] - // This is a non-standard iTunes extension - ?: id3v2["TSO2"] - ?: id3v2["TXXX:album artist sort"] - ) +fun TextTags.albumArtistSortNames() = + (vorbis["albumartistssort"] + ?: vorbis["albumartists_sort"] + ?: vorbis["albumartists sort"] + ?: vorbis["albumartistsort"] + ?: vorbis["album artist sort"] + ?: id3v2["TXXX:albumartistssort"] + ?: id3v2["TXXX:albumartists_sort"] + ?: id3v2["TXXX:albumartists sort"] + ?: id3v2["TXXX:albumartistsort"] + // This is a non-standard iTunes extension + ?: id3v2["TSO2"] + ?: id3v2["TXXX:album artist sort"]) // Genre fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"] // Compilation Flag -fun TextTags.isCompilation() = (vorbis["compilation"] ?: vorbis["itunescompilation"] -?: id3v2["TCMP"] // This is a non-standard itunes extension -?: id3v2["TXXX:compilation"] ?: id3v2["TXXX:itunescompilation"] - ) - ?.let { - // Ignore invalid instances of this tag - it == listOf("1") - } +fun TextTags.isCompilation() = + (vorbis["compilation"] + ?: vorbis["itunescompilation"] + ?: id3v2["TCMP"] // This is a non-standard itunes extension + ?: id3v2["TXXX:compilation"] + ?: id3v2["TXXX:itunescompilation"]) + ?.let { + // Ignore invalid instances of this tag + it == listOf("1") + } // ReplayGain information -fun TextTags.replayGainTrackAdjustment() = (vorbis["r128_track_gain"]?.parseR128Adjustment() - ?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment() - ?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()) +fun TextTags.replayGainTrackAdjustment() = + (vorbis["r128_track_gain"]?.parseR128Adjustment() + ?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment() + ?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()) -fun TextTags.replayGainAlbumAdjustment() = (vorbis["r128_album_gain"]?.parseR128Adjustment() - ?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment() - ?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()) +fun TextTags.replayGainAlbumAdjustment() = + (vorbis["r128_album_gain"]?.parseR128Adjustment() + ?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment() + ?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()) private fun TextTags.parseId3v23Date(): Date? { // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY @@ -182,14 +221,10 @@ private fun TextTags.parseId3v23Date(): Date? { } private fun List.parseR128Adjustment() = - first() - .replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "") - .toFloatOrNull() - ?.nonZeroOrNull() - ?.run { - // Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale - this / 256f + 5 - } + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()?.run { + // Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale + this / 256f + 5 + } /** * Parse a ReplayGain adjustment into a float value. @@ -199,7 +234,6 @@ private fun List.parseR128Adjustment() = private fun List.parseReplayGainAdjustment() = first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() - val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") val COMPILATION_RELEASE_TYPES = listOf("compilation") @@ -207,4 +241,4 @@ val COMPILATION_RELEASE_TYPES = listOf("compilation") * Matches non-float information from ReplayGain adjustments. Derived from vanilla music: * https://github.com/vanilla-music/vanilla */ -val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } \ No newline at end of file +val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagUtil.kt index 1bbfe65a0..104bf7f69 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagUtil.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 Auxio Project - * ID3Genre.kt is part of Auxio. + * TagUtil.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 @@ -117,7 +117,6 @@ fun String.parseId3v2PositionField() = fun parseVorbisPositionField(pos: String?, total: String?) = transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull()) - /** * Transform a raw position + total field into a position a way that tolerates placeholder values. * @@ -132,4 +131,4 @@ fun transformPositionField(pos: Int?, total: Int?) = pos } else { null - } \ No newline at end of file + } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/fs/DeviceFiles.kt index 97da57fd1..41f290475 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/fs/DeviceFiles.kt @@ -107,4 +107,3 @@ constructor( DocumentsContract.Document.COLUMN_LAST_MODIFIED) } } - diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/PlaylistDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/PlaylistDatabase.kt index 3f1fcefd6..f32efc69a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/PlaylistDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/PlaylistDatabase.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * UserMusicDatabase.kt is part of Auxio. + * PlaylistDatabase.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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/StoredPlaylists.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/StoredPlaylists.kt index 139a2b3de..bf054baaf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/StoredPlaylists.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/playlists/StoredPlaylists.kt @@ -1,26 +1,36 @@ +/* + * Copyright (c) 2024 Auxio Project + * StoredPlaylists.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.explore.playlists +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import org.oxycblt.auxio.music.stack.explore.PlaylistFile -import org.oxycblt.auxio.music.stack.explore.SongPointer -import javax.inject.Inject interface StoredPlaylists { fun read(): Flow } -class StoredPlaylistsImpl @Inject constructor( - private val playlistDao: PlaylistDao -) : StoredPlaylists { - override fun read() = flow { - emitAll(playlistDao.readRawPlaylists() - .asFlow() - .map { - TODO() - }) - } -} \ No newline at end of file +class StoredPlaylistsImpl @Inject constructor(private val playlistDao: PlaylistDao) : + StoredPlaylists { + override fun read() = flow { emitAll(playlistDao.readRawPlaylists().asFlow().map { TODO() }) } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/InterpretModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/InterpretModule.kt index 9443b8320..9a81b70e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/InterpretModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/InterpretModule.kt @@ -26,5 +26,5 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface InterpretModule { - @Binds fun interpreter(factory: InterpreterImpl): Interpreter + @Binds fun interpreter(interpreter: InterpreterImpl): Interpreter } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpretation.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpretation.kt index 96a54d2b7..b3da26d43 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpretation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpretation.kt @@ -1,9 +1,24 @@ +/* + * Copyright (c) 2024 Auxio Project + * Interpretation.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators -data class Interpretation( - val nameFactory: Name.Known.Factory, - val separators: Separators -) +data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpreter.kt index 4655157d3..dd953fcc8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/Interpreter.kt @@ -1,5 +1,24 @@ +/* + * Copyright (c) 2024 Auxio Project + * Interpreter.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer @@ -30,17 +49,14 @@ interface Interpreter { ): MutableLibrary } -class InterpreterImpl( - private val preparer: Preparer -) : Interpreter { +class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Interpreter { override suspend fun interpret( audioFiles: Flow, playlistFiles: Flow, interpretation: Interpretation ): MutableLibrary { val preSongs = - preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main) - .buffer() + preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main).buffer() val genreLinker = GenreLinker() val genreLinkedSongs = genreLinker.register(preSongs).flowOn(Dispatchers.Main).buffer() val artistLinker = ArtistLinker() @@ -53,7 +69,8 @@ class InterpreterImpl( val artists = artistLinker.resolve() val albumLinker = AlbumLinker() val albumLinkedSongs = - albumLinker.register(artistLinkedSongs) + albumLinker + .register(artistLinkedSongs) .flowOn(Dispatchers.Main) .map { LinkedSongImpl(it) } .toList() @@ -62,13 +79,18 @@ class InterpreterImpl( return LibraryImpl(songs, albums, artists, genres) } + private data class LinkedSongImpl(private val albumLinkedSong: AlbumLinker.LinkedSong) : + LinkedSong { + override val preSong: PreSong + get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong - private data class LinkedSongImpl( - private val albumLinkedSong: AlbumLinker.LinkedSong - ) : LinkedSong { - override val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong - override val album: Linked get() = albumLinkedSong.album - override val artists: Linked, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists - override val genres: Linked, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres + override val album: Linked + get() = albumLinkedSong.album + + override val artists: Linked, SongImpl> + get() = albumLinkedSong.linkedArtistSong.artists + + override val genres: Linked, SongImpl> + get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/AlbumLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/AlbumLinker.kt index d583de771..74b8ee49d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/AlbumLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/AlbumLinker.kt @@ -1,69 +1,77 @@ +/* + * Copyright (c) 2024 Auxio Project + * AlbumLinker.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.map -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl -import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl -import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl -import org.oxycblt.auxio.music.stack.interpret.model.SongImpl -import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum -import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre -import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong import java.util.UUID - +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl +import org.oxycblt.auxio.music.stack.interpret.model.SongImpl class AlbumLinker { private val tree = mutableMapOf>() - fun register(linkedSongs: Flow) = linkedSongs.map { - val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase() - val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId - val albumLink = tree.getOrPut(nameKey) { mutableMapOf() } - .getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) } - albumLink.node.contributors.contribute(it.linkedAlbum) - LinkedSong(it, albumLink) - } + fun register(linkedSongs: Flow) = + linkedSongs.map { + val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase() + val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId + val albumLink = + tree + .getOrPut(nameKey) { mutableMapOf() } + .getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) } + albumLink.node.contributors.contribute(it.linkedAlbum) + LinkedSong(it, albumLink) + } fun resolve(): Collection = tree.values.flatMap { musicBrainzIdBundle -> - val only = - musicBrainzIdBundle.values.singleOrNull() + val only = musicBrainzIdBundle.values.singleOrNull() if (only != null) { return@flatMap listOf(only.node.resolve()) } - val nullBundle = musicBrainzIdBundle[null] - ?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } + val nullBundle = + musicBrainzIdBundle[null] + ?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } // Only partially tagged with MBIDs, must go through and - musicBrainzIdBundle.filter { it.key != null }.forEach { - val candidates = it.value.node.contributors.candidates - nullBundle.node.contributors.contribute(candidates) - it.value.node = nullBundle.node - } + musicBrainzIdBundle + .filter { it.key != null } + .forEach { + val candidates = it.value.node.contributors.candidates + nullBundle.node.contributors.contribute(candidates) + it.value.node = nullBundle.node + } listOf(nullBundle.node.resolve()) } data class LinkedSong( - val linkedSong: ArtistLinker.LinkedSong, + val linkedArtistSong: ArtistLinker.LinkedSong, val album: Linked ) - private data class AlbumLink( - var node: AlbumNode - ) : Linked { + private data class AlbumLink(var node: AlbumNode) : Linked { override fun resolve(child: SongImpl): AlbumImpl { - return requireNotNull(node.albumImpl) { "Album not resolved yet" }.also { - it.link(child) - } + return requireNotNull(node.albumImpl) { "Album not resolved yet" } + .also { it.link(child) } } } - private class AlbumNode( - val contributors: Contribution - ) { + private class AlbumNode(val contributors: Contribution) { var albumImpl: AlbumImpl? = null private set @@ -74,9 +82,8 @@ class AlbumLinker { } } - private class LinkedAlbumImpl( - private val artistLinkedAlbum: ArtistLinker.LinkedAlbum - ) : LinkedAlbum { + private class LinkedAlbumImpl(private val artistLinkedAlbum: ArtistLinker.LinkedAlbum) : + LinkedAlbum { override val preAlbum = artistLinkedAlbum.preAlbum override val artists = artistLinkedAlbum.artists diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/ArtistLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/ArtistLinker.kt index 54a4e363b..33fa568bb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/ArtistLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/ArtistLinker.kt @@ -1,62 +1,81 @@ +/* + * Copyright (c) 2024 Auxio Project + * ArtistLinker.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker +import java.util.UUID import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map -import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl -import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum import org.oxycblt.auxio.music.stack.interpret.prepare.PreArtist -import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre -import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong -import java.util.UUID - class ArtistLinker { private val tree = mutableMapOf>() - fun register(linkedSongs: Flow) = linkedSongs.map { - val linkedSongArtists = it.preSong.preArtists.map { artist -> - val nameKey = artist.rawName?.lowercase() - val musicBrainzIdKey = artist.musicBrainzId - val artistLink = tree.getOrPut(nameKey) { mutableMapOf() } - .getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) } - artistLink.node.contributors.contribute(artist) - artistLink + fun register(linkedSongs: Flow) = + linkedSongs.map { + val linkedSongArtists = + it.preSong.preArtists.map { artist -> + val nameKey = artist.rawName?.lowercase() + val musicBrainzIdKey = artist.musicBrainzId + val artistLink = + tree + .getOrPut(nameKey) { mutableMapOf() } + .getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) } + artistLink.node.contributors.contribute(artist) + artistLink + } + val linkedAlbumArtists = + it.preSong.preAlbum.preArtists.map { artist -> + val nameKey = artist.rawName?.lowercase() + val musicBrainzIdKey = artist.musicBrainzId + val artistLink = + tree + .getOrPut(nameKey) { mutableMapOf() } + .getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) } + artistLink.node.contributors.contribute(artist) + artistLink + } + val linkedAlbum = LinkedAlbum(it.preSong.preAlbum, MultiArtistLink(linkedAlbumArtists)) + LinkedSong(it, linkedAlbum, MultiArtistLink(linkedSongArtists)) } - val linkedAlbumArtists = it.preSong.preAlbum.preArtists.map { artist -> - val nameKey = artist.rawName?.lowercase() - val musicBrainzIdKey = artist.musicBrainzId - val artistLink = tree.getOrPut(nameKey) { mutableMapOf() } - .getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) } - artistLink.node.contributors.contribute(artist) - artistLink - } - val linkedAlbum = - LinkedAlbum(it.preSong.preAlbum, MultiArtistLink(linkedAlbumArtists)) - LinkedSong(it, linkedAlbum, MultiArtistLink(linkedSongArtists)) - } fun resolve(): Collection = tree.values.flatMap { musicBrainzIdBundle -> - val only = - musicBrainzIdBundle.values.singleOrNull() + val only = musicBrainzIdBundle.values.singleOrNull() if (only != null) { return@flatMap listOf(only.node.resolve()) } - val nullBundle = musicBrainzIdBundle[null] - ?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } + val nullBundle = + musicBrainzIdBundle[null] + ?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } // Only partially tagged with MBIDs, must go through and - musicBrainzIdBundle.filter { it.key != null }.forEach { - val candidates = it.value.node.contributors.candidates - nullBundle.node.contributors.contribute(candidates) - it.value.node = nullBundle.node - } + musicBrainzIdBundle + .filter { it.key != null } + .forEach { + val candidates = it.value.node.contributors.candidates + nullBundle.node.contributors.contribute(candidates) + it.value.node = nullBundle.node + } listOf(nullBundle.node.resolve()) } @@ -71,31 +90,27 @@ class ArtistLinker { val artists: Linked, AlbumImpl> ) - private class MultiArtistLink( - val links: List> - ) : Linked, T> { + private class MultiArtistLink(val links: List>) : + Linked, T> { override fun resolve(child: T): List { return links.map { it.resolve(child) }.distinct() } } - private data class ArtistLink( - var node: ArtistNode - ) : Linked { + private data class ArtistLink(var node: ArtistNode) : Linked { override fun resolve(child: Music): ArtistImpl { - return requireNotNull(node.artistImpl) { "Artist not resolved yet" }.also { - when (child) { - is SongImpl -> it.link(child) - is AlbumImpl -> it.link(child) - else -> error("Cannot link to child $child") + return requireNotNull(node.artistImpl) { "Artist not resolved yet" } + .also { + when (child) { + is SongImpl -> it.link(child) + is AlbumImpl -> it.link(child) + else -> error("Cannot link to child $child") + } } - } } } - private class ArtistNode( - val contributors: Contribution - ) { + private class ArtistNode(val contributors: Contribution) { var artistImpl: ArtistImpl? = null private set diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/Contribution.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/Contribution.kt index badef6647..b2bf11e42 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/Contribution.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/Contribution.kt @@ -1,9 +1,28 @@ +/* + * Copyright (c) 2024 Auxio Project + * Contribution.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker class Contribution { private val map = mutableMapOf() - val candidates: Collection get() = map.keys + val candidates: Collection + get() = map.keys fun contribute(key: T) { map[key] = map.getOrDefault(key, 0) + 1 @@ -14,5 +33,4 @@ class Contribution { } fun resolve() = map.maxByOrNull { it.value }?.key ?: error("Nothing was contributed") - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/GenreLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/GenreLinker.kt index fef2dc5c9..2792cc578 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/GenreLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/GenreLinker.kt @@ -1,9 +1,25 @@ +/* + * Copyright (c) 2024 Auxio Project + * GenreLinker.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transform import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre @@ -12,45 +28,37 @@ import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong class GenreLinker { private val tree = mutableMapOf() - fun register(preSong: Flow): Flow = preSong.map { - val genreLinks = it.preGenres.map { genre -> - val nameKey = genre.rawName?.lowercase() - val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) } - link.node.contributors.contribute(genre) - link + fun register(preSong: Flow): Flow = + preSong.map { + val genreLinks = + it.preGenres.map { genre -> + val nameKey = genre.rawName?.lowercase() + val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) } + link.node.contributors.contribute(genre) + link + } + LinkedSong(it, MultiGenreLink(genreLinks)) } - LinkedSong(it, MultiGenreLink(genreLinks)) - } - fun resolve() = - tree.values.map { it.node.resolve() } + fun resolve() = tree.values.map { it.node.resolve() } - data class LinkedSong( - val preSong: PreSong, - val genres: Linked, SongImpl> - ) + data class LinkedSong(val preSong: PreSong, val genres: Linked, SongImpl>) - private class MultiGenreLink( - val links: List> - ) : Linked, SongImpl> { + private class MultiGenreLink(val links: List>) : + Linked, SongImpl> { override fun resolve(child: SongImpl): List { return links.map { it.resolve(child) }.distinct() } } - private data class GenreLink( - var node: GenreNode - ) : Linked { + private data class GenreLink(var node: GenreNode) : Linked { override fun resolve(child: SongImpl): GenreImpl { - return requireNotNull(node.genreImpl) { "Genre not resolved yet" }.also { - it.link(child) - } + return requireNotNull(node.genreImpl) { "Genre not resolved yet" } + .also { it.link(child) } } } - private class GenreNode( - val contributors: Contribution - ) { + private class GenreNode(val contributors: Contribution) { var genreImpl: GenreImpl? = null private set diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/LinkedMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/LinkedMusic.kt index 7d98ee929..ef3ac713a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/LinkedMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/LinkedMusic.kt @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2024 Auxio Project + * LinkedMusic.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.explore.PlaylistFile import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/PlaylistLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/PlaylistLinker.kt index 3c1e5d455..68ac9689c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/PlaylistLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/linker/PlaylistLinker.kt @@ -1,15 +1,33 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaylistLinker.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.linker import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.oxycblt.auxio.music.stack.explore.PlaylistFile -import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl import org.oxycblt.auxio.music.stack.interpret.model.PlaylistImpl -import org.oxycblt.auxio.music.stack.interpret.model.SongImpl -import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong - class PlaylistLinker { - fun register(playlists: Flow, linkedSongs: Flow): Flow = emptyFlow() + fun register( + playlists: Flow, + linkedSongs: Flow + ): Flow = emptyFlow() + fun resolve(): Collection = setOf() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt index 60ce69f9e..ae16c744f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt @@ -15,9 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.stack.interpret.model +import kotlin.math.min import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -32,7 +33,6 @@ import org.oxycblt.auxio.music.stack.interpret.linker.LinkedSong import org.oxycblt.auxio.music.stack.interpret.prepare.PreArtist import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre import org.oxycblt.auxio.util.update -import kotlin.math.min /** * Library-backed implementation of [Song]. @@ -81,9 +81,7 @@ class SongImpl(linkedSong: LinkedSong) : Song { override fun hashCode() = hashCode override fun equals(other: Any?) = - other is SongImpl && - uid == other.uid && - preSong == other.preSong + other is SongImpl && uid == other.uid && preSong == other.preSong override fun toString() = "Song(uid=$uid, name=$name)" } @@ -123,10 +121,7 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album { // Since equality on public-facing music models is not identical to the tag equality, // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = - other is AlbumImpl && - uid == other.uid && - preAlbum == other.preAlbum && - songs == other.songs + other is AlbumImpl && uid == other.uid && preAlbum == other.preAlbum && songs == other.songs override fun toString() = "Album(uid=$uid, name=$name)" @@ -135,11 +130,11 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album { durationMs += song.durationMs dateAdded = min(dateAdded, song.dateAdded) if (song.date != null) { - dates = dates?.let { - if (song.date < it.min) Date.Range(song.date, it.max) - else if (song.date > it.max) Date.Range(it.min, song.date) - else it - } ?: Date.Range(song.date, song.date) + dates = + dates?.let { + if (song.date < it.min) Date.Range(song.date, it.max) + else if (song.date > it.max) Date.Range(it.min, song.date) else it + } ?: Date.Range(song.date, song.date) } hashCode = 31 * hashCode + song.hashCode() } @@ -173,7 +168,6 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist { override lateinit var explicitAlbums: Set override lateinit var implicitAlbums: Set - override lateinit var genres: List override var durationMs = 0L @@ -189,9 +183,9 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist { // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = other is ArtistImpl && - uid == other.uid && - preArtist == other.preArtist && - songs == other.songs + uid == other.uid && + preArtist == other.preArtist && + songs == other.songs override fun toString() = "Artist(uid=$uid, name=$name)" @@ -240,9 +234,7 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist { * * @author Alexander Capehart (OxygenCobalt) */ -class GenreImpl( - private val preGenre: PreGenre -) : Genre { +class GenreImpl(private val preGenre: PreGenre) : Genre { override val uid = Music.UID.auxio(MusicType.GENRES) { update(preGenre.rawName) } override val name = preGenre.name @@ -256,10 +248,7 @@ class GenreImpl( override fun hashCode() = hashCode override fun equals(other: Any?) = - other is GenreImpl && - uid == other.uid && - preGenre == other.preGenre && - songs == other.songs + other is GenreImpl && uid == other.uid && preGenre == other.preGenre && songs == other.songs override fun toString() = "Genre(uid=$uid, name=$name)" diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/Library.kt index f3f3a787f..90ea8110f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/Library.kt @@ -1,31 +1,41 @@ +/* + * Copyright (c) 2024 Auxio Project + * Library.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.model -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist - -interface Library { - val songs: Collection - val albums: Collection - val artists: Collection - val genres: Collection - val playlists: Collection - - fun findSong(uid: Music.UID): Song? - fun findAlbum(uid: Music.UID): Album? - fun findArtist(uid: Music.UID): Artist? - fun findGenre(uid: Music.UID): Genre? - fun findPlaylist(uid: Music.UID): Playlist? -} +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.stack.explore.fs.Path interface MutableLibrary : Library { suspend fun createPlaylist(name: String, songs: List): MutableLibrary + suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary + suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary + suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary + suspend fun deletePlaylist(playlist: Playlist): MutableLibrary } @@ -41,6 +51,10 @@ class LibraryImpl( TODO("Not yet implemented") } + override fun findSongByPath(path: Path): Song? { + TODO("Not yet implemented") + } + override fun findAlbum(uid: Music.UID): Album? { TODO("Not yet implemented") } @@ -57,6 +71,10 @@ class LibraryImpl( TODO("Not yet implemented") } + override fun findPlaylistByName(name: String): Playlist? { + TODO("Not yet implemented") + } + override suspend fun createPlaylist(name: String, songs: List): MutableLibrary { TODO("Not yet implemented") } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt index 52919a4c4..9816a6a77 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt @@ -19,13 +19,8 @@ package org.oxycblt.auxio.music.stack.interpret.model import org.oxycblt.auxio.image.extractor.ParentCover -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.music.stack.explore.PlaylistFile -import org.oxycblt.auxio.music.stack.explore.playlists.RawPlaylist import org.oxycblt.auxio.music.stack.interpret.linker.LinkedPlaylist class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist { diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/ID3Genre.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/ID3Genre.kt index d0a0fc4df..1c36e9a11 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/ID3Genre.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/ID3Genre.kt @@ -1,8 +1,25 @@ +/* + * Copyright (c) 2024 Auxio Project + * ID3Genre.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.prepare /// --- ID3v2 PARSING --- - /** * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * representations of genre fields into their named counterparts, and split up singular ID3v2-style @@ -38,7 +55,7 @@ private fun String.parseId3v1Genre(): String? { // try to index the genre table with such. val numeric = toIntOrNull() - // Not a numeric value, try some other fixed values. + // Not a numeric value, try some other fixed values. ?: return when (this) { // CR and RX are not technically ID3v1, but are formatted similarly to a plain // number. diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt index ffa5c5ff9..ccb023ad5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt @@ -1,17 +1,34 @@ +/* + * Copyright (c) 2024 Auxio Project + * PreMusic.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.prepare import android.net.Uri +import java.util.UUID import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.music.stack.explore.PlaylistFile import org.oxycblt.auxio.music.stack.explore.PlaylistHandle import org.oxycblt.auxio.music.stack.explore.fs.MimeType import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment -import java.util.UUID data class PreSong( val musicBrainzId: UUID?, @@ -52,8 +69,4 @@ data class PreGenre( val rawName: String?, ) -data class PrePlaylist( - val name: Name.Known, - val rawName: String?, - val handle: PlaylistHandle -) \ No newline at end of file +data class PrePlaylist(val name: Name.Known, val rawName: String?, val handle: PlaylistHandle) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PrepareModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PrepareModule.kt index ba9dd1490..fbc6d88ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PrepareModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PrepareModule.kt @@ -1,12 +1,27 @@ +/* + * Copyright (c) 2024 Auxio Project + * PrepareModule.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.prepare - import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.music.stack.interpret.Interpreter -import org.oxycblt.auxio.music.stack.interpret.InterpreterImpl @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt index f90d8b448..81408bbcb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt @@ -1,5 +1,24 @@ +/* + * Copyright (c) 2024 Auxio Project + * Preparer.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.stack.interpret.prepare +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.oxycblt.auxio.R @@ -7,7 +26,6 @@ import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.fs.MimeType import org.oxycblt.auxio.music.stack.interpret.Interpretation @@ -18,128 +36,129 @@ interface Preparer { fun prepare(audioFiles: Flow, interpretation: Interpretation): Flow } -class PreparerImpl( - private val nameFactory: Name.Known.Factory, - private val separators: Separators -) : Preparer { - override fun prepare(audioFiles: Flow, interpretation: Interpretation) = audioFiles.map { audioFile -> - val individualPreArtists = makePreArtists( - audioFile.artistMusicBrainzIds, - audioFile.artistNames, - audioFile.artistSortNames - ) - val albumPreArtists = makePreArtists( - audioFile.albumArtistMusicBrainzIds, - audioFile.albumArtistNames, - audioFile.albumArtistSortNames - ) - val preAlbum = makePreAlbum(audioFile, individualPreArtists, albumPreArtists) - val rawArtists = - individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } - val rawGenres = - makePreGenres(audioFile).ifEmpty { listOf(unknownPreGenre()) } - val uri = audioFile.deviceFile.uri - PreSong( - musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(), - name = nameFactory.parse(need(audioFile, "name", audioFile.name), audioFile.sortName), - rawName = audioFile.name, - track = audioFile.track, - disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) }, - date = audioFile.date, - uri = uri, - cover = inferCover(audioFile), - path = need(audioFile, "path", audioFile.deviceFile.path), - mimeType = MimeType( - need(audioFile, "mime type", audioFile.deviceFile.mimeType), - null - ), - size = audioFile.deviceFile.size, - durationMs = need(audioFile, "duration", audioFile.durationMs), - replayGainAdjustment = ReplayGainAdjustment( - audioFile.replayGainTrackAdjustment, - audioFile.replayGainAlbumAdjustment, - ), - // TODO: Figure out what to do with date added - dateAdded = audioFile.deviceFile.lastModified, - preAlbum = preAlbum, - preArtists = rawArtists, - preGenres = rawGenres - ) - } +class PreparerImpl @Inject constructor() : Preparer { + override fun prepare(audioFiles: Flow, interpretation: Interpretation) = + audioFiles.map { audioFile -> + val individualPreArtists = + makePreArtists( + audioFile.artistMusicBrainzIds, + audioFile.artistNames, + audioFile.artistSortNames, + interpretation) + val albumPreArtists = + makePreArtists( + audioFile.albumArtistMusicBrainzIds, + audioFile.albumArtistNames, + audioFile.albumArtistSortNames, + interpretation) + val preAlbum = + makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation) + val rawArtists = + individualPreArtists + .ifEmpty { albumPreArtists } + .ifEmpty { listOf(unknownPreArtist()) } + val rawGenres = + makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) } + val uri = audioFile.deviceFile.uri + PreSong( + musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(), + name = + interpretation.nameFactory.parse( + need(audioFile, "name", audioFile.name), audioFile.sortName), + rawName = audioFile.name, + track = audioFile.track, + disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) }, + date = audioFile.date, + uri = uri, + cover = inferCover(audioFile), + path = need(audioFile, "path", audioFile.deviceFile.path), + mimeType = + MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null), + size = audioFile.deviceFile.size, + durationMs = need(audioFile, "duration", audioFile.durationMs), + replayGainAdjustment = + ReplayGainAdjustment( + audioFile.replayGainTrackAdjustment, + audioFile.replayGainAlbumAdjustment, + ), + // TODO: Figure out what to do with date added + dateAdded = audioFile.deviceFile.lastModified, + preAlbum = preAlbum, + preArtists = rawArtists, + preGenres = rawGenres) + } private fun need(audioFile: AudioFile, what: String, value: T?) = requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" } private fun inferCover(audioFile: AudioFile): Cover { - return Cover.Embedded( - audioFile.deviceFile.uri, - audioFile.deviceFile.uri, - "" - ) + return Cover.Embedded(audioFile.deviceFile.uri, audioFile.deviceFile.uri, "") } private fun makePreAlbum( audioFile: AudioFile, individualPreArtists: List, - albumPreArtists: List + albumPreArtists: List, + interpretation: Interpretation ): PreAlbum { val rawAlbumName = need(audioFile, "album name", audioFile.albumName) return PreAlbum( musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(), - name = nameFactory.parse(rawAlbumName, audioFile.albumSortName), + name = interpretation.nameFactory.parse(rawAlbumName, audioFile.albumSortName), rawName = rawAlbumName, - releaseType = ReleaseType.parse(separators.split(audioFile.releaseTypes)) - ?: ReleaseType.Album(null), + releaseType = + ReleaseType.parse(interpretation.separators.split(audioFile.releaseTypes)) + ?: ReleaseType.Album(null), preArtists = - albumPreArtists - .ifEmpty { individualPreArtists } - .ifEmpty { listOf(unknownPreArtist()) }) + albumPreArtists + .ifEmpty { individualPreArtists } + .ifEmpty { listOf(unknownPreArtist()) }) } private fun makePreArtists( rawMusicBrainzIds: List, rawNames: List, - rawSortNames: List + rawSortNames: List, + interpretation: Interpretation ): List { - val musicBrainzIds = separators.split(rawMusicBrainzIds) - val names = separators.split(rawNames) - val sortNames = separators.split(rawSortNames) - return names - .mapIndexed { i, name -> - makePreArtist( - musicBrainzIds.getOrNull(i), - name, - sortNames.getOrNull(i) - ) - } - + val musicBrainzIds = interpretation.separators.split(rawMusicBrainzIds) + val names = interpretation.separators.split(rawNames) + val sortNames = interpretation.separators.split(rawSortNames) + return names.mapIndexed { i, name -> + makePreArtist(musicBrainzIds.getOrNull(i), name, sortNames.getOrNull(i), interpretation) + } } private fun makePreArtist( musicBrainzId: String?, rawName: String?, - sortName: String? + sortName: String?, + interpretation: Interpretation ): PreArtist { val name = - rawName?.let { nameFactory.parse(it, sortName) } ?: Name.Unknown(R.string.def_artist) + rawName?.let { interpretation.nameFactory.parse(it, sortName) } + ?: Name.Unknown(R.string.def_artist) val musicBrainzId = musicBrainzId?.toUuidOrNull() return PreArtist(musicBrainzId, name, rawName) } - private fun unknownPreArtist() = - PreArtist(null, Name.Unknown(R.string.def_artist), null) + private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null) - private fun makePreGenres(audioFile: AudioFile): List { + private fun makePreGenres( + audioFile: AudioFile, + interpretation: Interpretation + ): List { val genreNames = - audioFile.genreNames.parseId3GenreNames() ?: separators.split(audioFile.genreNames) - return genreNames.map { makePreGenre(it) } + audioFile.genreNames.parseId3GenreNames() + ?: interpretation.separators.split(audioFile.genreNames) + return genreNames.map { makePreGenre(it, interpretation) } } - private fun makePreGenre(rawName: String?) = - PreGenre(rawName?.let { nameFactory.parse(it, null) } ?: Name.Unknown(R.string.def_genre), + private fun makePreGenre(rawName: String?, interpretation: Interpretation) = + PreGenre( + rawName?.let { interpretation.nameFactory.parse(it, null) } + ?: Name.Unknown(R.string.def_genre), rawName) - private fun unknownPreGenre() = - PreGenre(Name.Unknown(R.string.def_genre), null) - -} \ No newline at end of file + private fun unknownPreGenre() = PreGenre(Name.Unknown(R.string.def_genre), null) +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt index 21af44a86..bc276a20b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt @@ -48,8 +48,8 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M override fun onMusicChanges(changes: MusicRepository.Changes) { if (!changes.deviceLibrary) return - val deviceLibrary = musicRepository.deviceLibrary ?: return - _currentPickerSong.value = _currentPickerSong.value?.run { deviceLibrary.findSong(uid) } + val library = musicRepository.library ?: return + _currentPickerSong.value = _currentPickerSong.value?.run { library.findSong(uid) } } override fun onCleared() { @@ -64,7 +64,7 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M */ fun setPickerSongUid(uid: Music.UID) { L.d("Opening picker for song $uid") - _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) + _currentPickerSong.value = musicRepository.library?.findSong(uid) if (_currentPickerSong.value != null) { L.w("Given song UID was invalid") } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index 6fbbbdb71..60df8e3da 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -50,7 +50,7 @@ constructor( ) : PersistenceRepository { override suspend fun readState(): PlaybackStateManager.SavedState? { - val deviceLibrary = musicRepository.deviceLibrary ?: return null + val library = musicRepository.library ?: return null val playbackState: PlaybackState val heapItems: List val mappingItems: List @@ -64,7 +64,7 @@ constructor( return null } - val heap = heapItems.map { deviceLibrary.findSong(it.uid) } + val heap = heapItems.map { library.findSong(it.uid) } val shuffledMapping = mappingItems.map { it.index } val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 724da06ac..5f87d0a96 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -130,8 +130,8 @@ class ExoPlaybackStateHolder( get() = player.audioSessionId override fun resolveQueue(): RawQueue { - val deviceLibrary = - musicRepository.deviceLibrary + val library = + musicRepository.library // No library, cannot do anything. ?: return RawQueue(emptyList(), emptyList(), 0) val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) } @@ -145,8 +145,8 @@ class ExoPlaybackStateHolder( } override fun handleDeferred(action: DeferredPlayback): Boolean { - val deviceLibrary = - musicRepository.deviceLibrary + val library = + musicRepository.library // No library, cannot do anything. ?: return false @@ -181,12 +181,13 @@ class ExoPlaybackStateHolder( // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { L.d("Opening specified file") - deviceLibrary.findSongForUri(context, action.uri)?.let { song -> - playbackManager.play( - requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { - "Invalid playback parameters" - }) - } + // library.findSongForUri(context, action.uri)?.let { song -> + // playbackManager.play( + // requireNotNull(commandFactory.song(song, + // ShuffleMode.IMPLICIT)) { + // "Invalid playback parameters" + // }) + // } } } @@ -498,7 +499,7 @@ class ExoPlaybackStateHolder( // --- MUSICREPOSITORY METHODS --- override fun onMusicChanges(changes: MusicRepository.Changes) { - if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { + if (changes.deviceLibrary && musicRepository.library != null) { // We now have a library, see if we have anything we need to do. L.d("Library obtained, requesting action") playbackManager.requestAction(this) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index c3b85b9a1..03afc4363 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -32,16 +32,15 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MusicBrowser -import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode @@ -92,23 +91,21 @@ constructor( override fun onPlayFromSearch(query: String, extras: Bundle) { super.onPlayFromSearch(query, extras) - val deviceLibrary = musicRepository.deviceLibrary ?: return - val userLibrary = musicRepository.userLibrary ?: return - val command = - expandSearchInfoCommand(query.ifBlank { null }, extras, deviceLibrary, userLibrary) + val library = musicRepository.library ?: return + val command = expandSearchInfoCommand(query.ifBlank { null }, extras, library) playbackManager.play(requireNotNull(command) { "Invalid playback configuration" }) } override fun onAddQueueItem(description: MediaDescriptionCompat) { super.onAddQueueItem(description) - val deviceLibrary = musicRepository.deviceLibrary ?: return + val library = musicRepository.library ?: return val uid = MediaSessionUID.fromString(description.mediaId ?: return) ?: return val songUid = when (uid) { is MediaSessionUID.SingleItem -> uid.uid else -> return } - val song = deviceLibrary.songs.find { it.uid == songUid } ?: return + val song = library.songs.find { it.uid == songUid } ?: return playbackManager.addToQueue(song) } @@ -208,8 +205,7 @@ constructor( private fun expandSearchInfoCommand( query: String?, extras: Bundle, - deviceLibrary: DeviceLibrary, - userLibrary: UserLibrary + library: Library ): PlaybackCommand? { if (query == null) { // User just wanted to 'play some music', shuffle all @@ -222,7 +218,7 @@ constructor( val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) val best = - deviceLibrary.songs.maxByOrNull { + library.songs.maxByOrNull { fuzzy(it.name, songQuery) + fuzzy(it.album.name, albumQuery) + it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) } @@ -235,7 +231,7 @@ constructor( val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) val best = - deviceLibrary.albums.maxByOrNull { + library.albums.maxByOrNull { fuzzy(it.name, albumQuery) + it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) } } @@ -245,21 +241,21 @@ constructor( } MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> { val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) - val best = deviceLibrary.artists.maxByOrNull { fuzzy(it.name, artistQuery) } + val best = library.artists.maxByOrNull { fuzzy(it.name, artistQuery) } if (best != null) { return commandFactory.artist(best, ShuffleMode.OFF) } } MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> { val genreQuery = extras.getString(MediaStore.EXTRA_MEDIA_GENRE) - val best = deviceLibrary.genres.maxByOrNull { fuzzy(it.name, genreQuery) } + val best = library.genres.maxByOrNull { fuzzy(it.name, genreQuery) } if (best != null) { return commandFactory.genre(best, ShuffleMode.OFF) } } MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE -> { val playlistQuery = extras.getString(MediaStore.EXTRA_MEDIA_PLAYLIST) - val best = userLibrary.playlists.maxByOrNull { fuzzy(it.name, playlistQuery) } + val best = library.playlists.maxByOrNull { fuzzy(it.name, playlistQuery) } if (best != null) { return commandFactory.playlist(best, ShuffleMode.OFF) } @@ -268,11 +264,7 @@ constructor( } val bestMusic = - (deviceLibrary.songs + - deviceLibrary.albums + - deviceLibrary.artists + - deviceLibrary.genres + - userLibrary.playlists) + (library.songs + library.albums + library.artists + library.genres + library.playlists) .maxByOrNull { fuzzy(it.name, query) } // TODO: Error out when we can't correctly resolve the query return bestMusic?.let { expandMusicIntoCommand(it, null) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt index 14b87c1dd..05bc985e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -147,8 +147,8 @@ constructor( } private fun newCommand(song: Song?, shuffle: ShuffleMode): PlaybackCommand? { - val deviceLibrary = musicRepository.deviceLibrary ?: return null - return newCommand(song, null, deviceLibrary.songs, listSettings.songSort, shuffle) + val library = musicRepository.library ?: return null + return newCommand(song, null, library.songs, listSettings.songSort, shuffle) } private fun newCommand( diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 6a0ab9240..ee72eab94 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -33,11 +33,10 @@ import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.PlainDivider import org.oxycblt.auxio.list.sort.Sort +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.model.DeviceLibrary -import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import timber.log.Timber as L @@ -95,9 +94,8 @@ constructor( currentSearchJob?.cancel() lastQuery = query - val deviceLibrary = musicRepository.deviceLibrary - val userLibrary = musicRepository.userLibrary - if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) { + val library = musicRepository.library + if (query.isNullOrEmpty() || library == null) { L.d("Cannot search for the current query, aborting") _searchResults.value = listOf() return @@ -107,16 +105,11 @@ constructor( L.d("Searching music library for $query") currentSearchJob = viewModelScope.launch { - _searchResults.value = - searchImpl(deviceLibrary, userLibrary, query).also { yield() } + _searchResults.value = searchImpl(library, query).also { yield() } } } - private suspend fun searchImpl( - deviceLibrary: DeviceLibrary, - userLibrary: UserLibrary, - query: String - ): List { + private suspend fun searchImpl(library: Library, query: String): List { val filter = searchSettings.filterTo val items = @@ -124,19 +117,19 @@ constructor( // A nulled filter type means to not filter anything. L.d("No filter specified, using entire library") SearchEngine.Items( - deviceLibrary.songs, - deviceLibrary.albums, - deviceLibrary.artists, - deviceLibrary.genres, - userLibrary.playlists) + library.songs, + library.albums, + library.artists, + library.genres, + library.playlists) } else { L.d("Filter specified, reducing library") SearchEngine.Items( - songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null, - albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null, - artists = if (filter == MusicType.ARTISTS) deviceLibrary.artists else null, - genres = if (filter == MusicType.GENRES) deviceLibrary.genres else null, - playlists = if (filter == MusicType.PLAYLISTS) userLibrary.playlists else null) + songs = if (filter == MusicType.SONGS) library.songs else null, + albums = if (filter == MusicType.ALBUMS) library.albums else null, + artists = if (filter == MusicType.ARTISTS) library.artists else null, + genres = if (filter == MusicType.GENRES) library.genres else null, + playlists = if (filter == MusicType.PLAYLISTS) library.playlists else null) } val results = searchEngine.search(items, query) diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt deleted file mode 100644 index 8ec814c93..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * CacheRepositoryTest.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music.cache - -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerifyAll -import io.mockk.coVerifySequence -import io.mockk.just -import io.mockk.mockk -import io.mockk.slot -import java.lang.IllegalStateException -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.oxycblt.auxio.music.stack.explore.AudioFile -import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.music.stack.explore.cache.TagDao -import org.oxycblt.auxio.music.stack.explore.cache.Tags - -class CacheRepositoryTest { - @Test - fun cache_read_noInvalidate() { - val dao = - mockk { - coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) - } - val cacheRepository = CacheRepositoryImpl(dao) - val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - assertFalse(cache.invalidated) - - val songA = AudioFile(mediaStoreId = 0, dateAdded = 1, dateModified = 2) - assertTrue(cache.populate(songA)) - assertEquals(RAW_SONG_A, songA) - - assertFalse(cache.invalidated) - - val songB = AudioFile(mediaStoreId = 9, dateAdded = 10, dateModified = 11) - assertTrue(cache.populate(songB)) - assertEquals(RAW_SONG_B, songB) - - assertFalse(cache.invalidated) - } - - @Test - fun cache_read_invalidate() { - val dao = - mockk { - coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) - } - val cacheRepository = CacheRepositoryImpl(dao) - val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - assertFalse(cache.invalidated) - - val nullStart = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0) - val nullEnd = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0) - assertFalse(cache.populate(nullStart)) - assertEquals(nullStart, nullEnd) - - assertTrue(cache.invalidated) - - val songB = AudioFile(mediaStoreId = 9, dateAdded = 10, dateModified = 11) - assertTrue(cache.populate(songB)) - assertEquals(RAW_SONG_B, songB) - - assertTrue(cache.invalidated) - } - - @Test - fun cache_read_crashes() { - val dao = mockk { coEvery { readSongs() } throws IllegalStateException() } - val cacheRepository = CacheRepositoryImpl(dao) - assertEquals(null, runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - } - - @Test - fun cache_write() { - var currentlyStoredSongs = listOf() - val insertSongsArg = slot>() - val dao = - mockk { - coEvery { nukeSongs() } answers { currentlyStoredSongs = listOf() } - - coEvery { insertSongs(capture(insertSongsArg)) } answers - { - currentlyStoredSongs = insertSongsArg.captured - } - } - - val cacheRepository = CacheRepositoryImpl(dao) - - val rawSongs = listOf(RAW_SONG_A, RAW_SONG_B) - runBlocking { cacheRepository.writeCache(rawSongs) } - - val cachedSongs = listOf(CACHED_SONG_A, CACHED_SONG_B) - coVerifySequence { - dao.nukeSongs() - dao.insertSongs(cachedSongs) - } - assertEquals(cachedSongs, currentlyStoredSongs) - } - - @Test - fun cache_write_nukeCrashes() { - val dao = - mockk { - coEvery { nukeSongs() } throws IllegalStateException() - coEvery { insertSongs(listOf()) } just Runs - } - val cacheRepository = CacheRepositoryImpl(dao) - runBlocking { cacheRepository.writeCache(listOf()) } - coVerifyAll { dao.nukeSongs() } - } - - @Test - fun cache_write_insertCrashes() { - val dao = - mockk { - coEvery { nukeSongs() } just Runs - coEvery { insertSongs(listOf()) } throws IllegalStateException() - } - val cacheRepository = CacheRepositoryImpl(dao) - runBlocking { cacheRepository.writeCache(listOf()) } - coVerifySequence { - dao.nukeSongs() - dao.insertSongs(listOf()) - } - } - - private companion object { - val CACHED_SONG_A = - Tags( - mediaStoreId = 0, - dateAdded = 1, - dateModified = 2, - size = 3, - durationMs = 4, - replayGainTrackAdjustment = 5.5f, - replayGainAlbumAdjustment = 6.6f, - musicBrainzId = "Song MBID A", - name = "Song Name A", - sortName = "Song Sort Name A", - track = 7, - disc = 8, - subtitle = "Subtitle A", - date = Date.from("2020-10-10"), - albumMusicBrainzId = "Album MBID A", - albumName = "Album Name A", - albumSortName = "Album Sort Name A", - releaseTypes = listOf("Release Type A"), - artistMusicBrainzIds = listOf("Artist MBID A"), - artistNames = listOf("Artist Name A"), - artistSortNames = listOf("Artist Sort Name A"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), - albumArtistNames = listOf("Album Artist Name A"), - albumArtistSortNames = listOf("Album Artist Sort Name A"), - genreNames = listOf("Genre Name A"), - ) - - val RAW_SONG_A = - AudioFile( - mediaStoreId = 0, - dateAdded = 1, - dateModified = 2, - size = 3, - durationMs = 4, - replayGainTrackAdjustment = 5.5f, - replayGainAlbumAdjustment = 6.6f, - musicBrainzId = "Song MBID A", - name = "Song Name A", - sortName = "Song Sort Name A", - track = 7, - disc = 8, - subtitle = "Subtitle A", - date = Date.from("2020-10-10"), - albumMusicBrainzId = "Album MBID A", - albumName = "Album Name A", - albumSortName = "Album Sort Name A", - releaseTypes = listOf("Release Type A"), - artistMusicBrainzIds = listOf("Artist MBID A"), - artistNames = listOf("Artist Name A"), - artistSortNames = listOf("Artist Sort Name A"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), - albumArtistNames = listOf("Album Artist Name A"), - albumArtistSortNames = listOf("Album Artist Sort Name A"), - genreNames = listOf("Genre Name A"), - ) - - val CACHED_SONG_B = - Tags( - mediaStoreId = 9, - dateAdded = 10, - dateModified = 11, - size = 12, - durationMs = 13, - replayGainTrackAdjustment = 14.14f, - replayGainAlbumAdjustment = 15.15f, - musicBrainzId = "Song MBID B", - name = "Song Name B", - sortName = "Song Sort Name B", - track = 16, - disc = 17, - subtitle = "Subtitle B", - date = Date.from("2021-11-11"), - albumMusicBrainzId = "Album MBID B", - albumName = "Album Name B", - albumSortName = "Album Sort Name B", - releaseTypes = listOf("Release Type B"), - artistMusicBrainzIds = listOf("Artist MBID B"), - artistNames = listOf("Artist Name B"), - artistSortNames = listOf("Artist Sort Name B"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), - albumArtistNames = listOf("Album Artist Name B"), - albumArtistSortNames = listOf("Album Artist Sort Name B"), - genreNames = listOf("Genre Name B"), - ) - - val RAW_SONG_B = - AudioFile( - mediaStoreId = 9, - dateAdded = 10, - dateModified = 11, - size = 12, - durationMs = 13, - replayGainTrackAdjustment = 14.14f, - replayGainAlbumAdjustment = 15.15f, - musicBrainzId = "Song MBID B", - name = "Song Name B", - sortName = "Song Sort Name B", - track = 16, - disc = 17, - subtitle = "Subtitle B", - date = Date.from("2021-11-11"), - albumMusicBrainzId = "Album MBID B", - albumName = "Album Name B", - albumSortName = "Album Sort Name B", - releaseTypes = listOf("Release Type B"), - artistMusicBrainzIds = listOf("Artist MBID B"), - artistNames = listOf("Artist Name B"), - artistSortNames = listOf("Artist Sort Name B"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), - albumArtistNames = listOf("Album Artist Name B"), - albumArtistSortNames = listOf("Album Artist Sort Name B"), - genreNames = listOf("Genre Name B"), - ) - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt index 65a24edd2..b000313c0 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt @@ -21,10 +21,10 @@ package org.oxycblt.auxio.music.metadata import org.junit.Assert.assertEquals import org.junit.Test import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace -import org.oxycblt.auxio.music.stack.explore.extractor.parseId3GenreNames import org.oxycblt.auxio.music.stack.explore.extractor.parseId3v2PositionField import org.oxycblt.auxio.music.stack.explore.extractor.parseVorbisPositionField import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped +import org.oxycblt.auxio.music.stack.interpret.prepare.parseId3GenreNames class TagUtilTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt deleted file mode 100644 index 686990894..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DeviceLibraryTest.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music.user - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Test -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicType -import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl -import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl -import org.oxycblt.auxio.music.model.DeviceLibraryImpl -import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl -import org.oxycblt.auxio.music.stack.interpret.model.SongImpl -import org.oxycblt.auxio.music.stack.explore.fs.Components -import org.oxycblt.auxio.music.stack.explore.fs.Path - -class DeviceLibraryTest { - - @Test - fun deviceLibrary_withSongs() { - val songUidA = Music.UID.auxio(MusicType.SONGS) - val songUidB = Music.UID.auxio(MusicType.SONGS) - val songA = - mockk { - every { uid } returns songUidA - every { durationMs } returns 0 - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val songB = - mockk { - every { uid } returns songUidB - every { durationMs } returns 1 - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(songA, songB), listOf(), listOf(), listOf()) - verify { - songA.finalize() - songB.finalize() - } - val foundSongA = deviceLibrary.findSong(songUidA)!! - assertEquals(songUidA, foundSongA.uid) - assertEquals(0L, foundSongA.durationMs) - val foundSongB = deviceLibrary.findSong(songUidB)!! - assertEquals(songUidB, foundSongB.uid) - assertEquals(1L, foundSongB.durationMs) - } - - @Test - fun deviceLibrary_withAlbums() { - val albumUidA = Music.UID.auxio(MusicType.ALBUMS) - val albumUidB = Music.UID.auxio(MusicType.ALBUMS) - val albumA = - mockk { - every { uid } returns albumUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val albumB = - mockk { - every { uid } returns albumUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(albumA, albumB), listOf(), listOf()) - verify { - albumA.finalize() - albumB.finalize() - } - val foundAlbumA = deviceLibrary.findAlbum(albumUidA)!! - assertEquals(albumUidA, foundAlbumA.uid) - assertEquals(0L, foundAlbumA.durationMs) - val foundAlbumB = deviceLibrary.findAlbum(albumUidB)!! - assertEquals(albumUidB, foundAlbumB.uid) - assertEquals(1L, foundAlbumB.durationMs) - } - - @Test - fun deviceLibrary_withArtists() { - val artistUidA = Music.UID.auxio(MusicType.ARTISTS) - val artistUidB = Music.UID.auxio(MusicType.ARTISTS) - val artistA = - mockk { - every { uid } returns artistUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val artistB = - mockk { - every { uid } returns artistUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = - DeviceLibraryImpl(listOf(), listOf(), listOf(artistA, artistB), listOf()) - verify { - artistA.finalize() - artistB.finalize() - } - val foundArtistA = deviceLibrary.findArtist(artistUidA)!! - assertEquals(artistUidA, foundArtistA.uid) - assertEquals(0L, foundArtistA.durationMs) - val foundArtistB = deviceLibrary.findArtist(artistUidB)!! - assertEquals(artistUidB, foundArtistB.uid) - assertEquals(1L, foundArtistB.durationMs) - } - - @Test - fun deviceLibrary_withGenres() { - val genreUidA = Music.UID.auxio(MusicType.GENRES) - val genreUidB = Music.UID.auxio(MusicType.GENRES) - val genreA = - mockk { - every { uid } returns genreUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val genreB = - mockk { - every { uid } returns genreUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(), listOf(), listOf(genreA, genreB)) - verify { - genreA.finalize() - genreB.finalize() - } - val foundGenreA = deviceLibrary.findGenre(genreUidA)!! - assertEquals(genreUidA, foundGenreA.uid) - assertEquals(0L, foundGenreA.durationMs) - val foundGenreB = deviceLibrary.findGenre(genreUidB)!! - assertEquals(genreUidB, foundGenreB.uid) - assertEquals(1L, foundGenreB.durationMs) - } - - @Test - fun deviceLibrary_equals() { - val songA = - mockk { - every { uid } returns Music.UID.auxio(MusicType.SONGS) - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val songB = - mockk { - every { uid } returns Music.UID.auxio(MusicType.SONGS) - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val album = - mockk { - every { uid } returns mockk() - every { finalize() } returns this - } - - val deviceLibraryA = DeviceLibraryImpl(listOf(songA), listOf(album), listOf(), listOf()) - val deviceLibraryB = DeviceLibraryImpl(listOf(songA), listOf(), listOf(), listOf()) - val deviceLibraryC = DeviceLibraryImpl(listOf(songB), listOf(album), listOf(), listOf()) - assertEquals(deviceLibraryA, deviceLibraryB) - assertEquals(deviceLibraryA.hashCode(), deviceLibraryA.hashCode()) - assertNotEquals(deviceLibraryA, deviceLibraryC) - assertNotEquals(deviceLibraryA.hashCode(), deviceLibraryC.hashCode()) - } -}