music: connect new loader to rest of app

This commit is contained in:
Alexander Capehart 2024-11-26 09:35:14 -07:00
parent e3d6644634
commit ba29905aa6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
52 changed files with 915 additions and 1116 deletions

View file

@ -121,7 +121,7 @@ private class DetailGeneratorImpl(
}
override fun album(uid: Music.UID): Detail<Album>? {
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<Artist>? {
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<Genre>? {
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<Playlist>? {
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))

View file

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

View file

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

View file

@ -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<Tab.Visible>().map { it.type }

View file

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

View file

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

View file

@ -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<Song>
val albums: Collection<Album>
val artists: Collection<Artist>
val genres: Collection<Genre>
val playlists: Collection<Playlist>
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.

View file

@ -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<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@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<Song>) {
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<Song>, 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<Song>) {
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),

View file

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

View file

@ -89,13 +89,13 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
override fun onMusicChanges(changes: MusicRepository.Changes) {
var refreshChoicesWith: List<Song>? = 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<Music.UID>) {
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<Song>) {
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))
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Uri>,
interpretation: Interpretation
): MutableLibrary
suspend fun run(uris: List<Uri>, interpretation: Interpretation): MutableLibrary
}
class IndexerImpl
@Inject
constructor(
private val explorer: Explorer,
private val interpreter: Interpreter
) : Indexer {
override suspend fun run(
uris: List<Uri>,
interpretation: Interpretation
) = coroutineScope {
constructor(private val explorer: Explorer, private val interpreter: Interpreter) : Indexer {
override suspend fun run(uris: List<Uri>, interpretation: Interpretation) = coroutineScope {
val files = explorer.explore(uris)
val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer()
val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer()

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Uri>): Files
}
data class Files(
val audios: Flow<AudioFile>,
val playlists: Flow<PlaylistFile>
)
data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
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<CacheResult>.results(): Pair<Flow<DeviceFile>, Flow<AudioFile>> {
val shared = shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0)
val shared =
shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0)
val files = shared.filterIsInstance<CacheResult.Miss>().map { it.deviceFile }
val songs = shared.filterIsInstance<CacheResult.Hit>().map { it.audioFile }
return files to songs
@ -63,10 +81,9 @@ class ExplorerImpl @Inject constructor(
private fun <T> Flow<T>.split(n: Int): Array<Flow<T>> {
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 } }
}
}

View file

@ -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<Song>)
suspend fun rewrite(songs: List<Song>)
suspend fun delete()
}
sealed interface SongPointer {
data class UID(val uid: Music.UID) : SongPointer
// data class Path(val options: List<Path>) : SongPointer
// data class Path(val options: List<Path>) : SongPointer
}

View file

@ -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<DeviceFile>): Flow<CacheResult>

View file

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

View file

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.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<DeviceFile>
): Flow<AudioFile>
fun extract(deviceFiles: Flow<DeviceFile>): Flow<AudioFile>
}
class TagExtractorImpl @Inject constructor(
class TagExtractorImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
private val mediaSourceFactory: MediaSource.Factory,
) : TagExtractor {
override fun extract(
deviceFiles: Flow<DeviceFile>
) = flow {
override fun extract(deviceFiles: Flow<DeviceFile>) = 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 <T> need(a: T, called: String) =

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>.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<String>.parseR128Adjustment() =
private fun List<String>.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.-]") }
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }

View file

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

View file

@ -107,4 +107,3 @@ constructor(
DocumentsContract.Document.COLUMN_LAST_MODIFIED)
}
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<PlaylistFile>
}
class StoredPlaylistsImpl @Inject constructor(
private val playlistDao: PlaylistDao
) : StoredPlaylists {
override fun read() = flow {
emitAll(playlistDao.readRawPlaylists()
.asFlow()
.map {
TODO()
})
}
}
class StoredPlaylistsImpl @Inject constructor(private val playlistDao: PlaylistDao) :
StoredPlaylists {
override fun read() = flow { emitAll(playlistDao.readRawPlaylists().asFlow().map { TODO() }) }
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<AudioFile>,
playlistFiles: Flow<PlaylistFile>,
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<AlbumImpl, SongImpl> get() = albumLinkedSong.album
override val artists: Linked<List<ArtistImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists
override val genres: Linked<List<GenreImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
override val album: Linked<AlbumImpl, SongImpl>
get() = albumLinkedSong.album
override val artists: Linked<List<ArtistImpl>, SongImpl>
get() = albumLinkedSong.linkedArtistSong.artists
override val genres: Linked<List<GenreImpl>, SongImpl>
get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String?, MutableMap<UUID?, AlbumLink>>()
fun register(linkedSongs: Flow<ArtistLinker.LinkedSong>) = 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<ArtistLinker.LinkedSong>) =
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<AlbumImpl> =
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<AlbumImpl, SongImpl>
)
private data class AlbumLink(
var node: AlbumNode
) : Linked<AlbumImpl, SongImpl> {
private data class AlbumLink(var node: AlbumNode) : Linked<AlbumImpl, SongImpl> {
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<ArtistLinker.LinkedAlbum>
) {
private class AlbumNode(val contributors: Contribution<ArtistLinker.LinkedAlbum>) {
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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String?, MutableMap<UUID?, ArtistLink>>()
fun register(linkedSongs: Flow<GenreLinker.LinkedSong>) = 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<GenreLinker.LinkedSong>) =
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<ArtistImpl> =
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<List<ArtistImpl>, AlbumImpl>
)
private class MultiArtistLink<T : Music>(
val links: List<Linked<ArtistImpl, Music>>
) : Linked<List<ArtistImpl>, T> {
private class MultiArtistLink<T : Music>(val links: List<Linked<ArtistImpl, Music>>) :
Linked<List<ArtistImpl>, T> {
override fun resolve(child: T): List<ArtistImpl> {
return links.map { it.resolve(child) }.distinct()
}
}
private data class ArtistLink(
var node: ArtistNode
) : Linked<ArtistImpl, Music> {
private data class ArtistLink(var node: ArtistNode) : Linked<ArtistImpl, Music> {
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<PreArtist>
) {
private class ArtistNode(val contributors: Contribution<PreArtist>) {
var artistImpl: ArtistImpl? = null
private set

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.stack.interpret.linker
class Contribution<T> {
private val map = mutableMapOf<T, Int>()
val candidates: Collection<T> get() = map.keys
val candidates: Collection<T>
get() = map.keys
fun contribute(key: T) {
map[key] = map.getOrDefault(key, 0) + 1
@ -14,5 +33,4 @@ class Contribution<T> {
}
fun resolve() = map.maxByOrNull { it.value }?.key ?: error("Nothing was contributed")
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String?, GenreLink>()
fun register(preSong: Flow<PreSong>): Flow<LinkedSong> = 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<PreSong>): Flow<LinkedSong> =
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<List<GenreImpl>, SongImpl>
)
data class LinkedSong(val preSong: PreSong, val genres: Linked<List<GenreImpl>, SongImpl>)
private class MultiGenreLink(
val links: List<Linked<GenreImpl, SongImpl>>
) : Linked<List<GenreImpl>, SongImpl> {
private class MultiGenreLink(val links: List<Linked<GenreImpl, SongImpl>>) :
Linked<List<GenreImpl>, SongImpl> {
override fun resolve(child: SongImpl): List<GenreImpl> {
return links.map { it.resolve(child) }.distinct()
}
}
private data class GenreLink(
var node: GenreNode
) : Linked<GenreImpl, SongImpl> {
private data class GenreLink(var node: GenreNode) : Linked<GenreImpl, SongImpl> {
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<PreGenre>
) {
private class GenreNode(val contributors: Contribution<PreGenre>) {
var genreImpl: GenreImpl? = null
private set

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<PlaylistFile>, linkedSongs: Flow<AlbumLinker.LinkedSong>): Flow<LinkedPlaylist> = emptyFlow()
fun register(
playlists: Flow<PlaylistFile>,
linkedSongs: Flow<AlbumLinker.LinkedSong>
): Flow<LinkedPlaylist> = emptyFlow()
fun resolve(): Collection<PlaylistImpl> = setOf()
}

View file

@ -15,9 +15,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.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<Album>
override lateinit var implicitAlbums: Set<Album>
override lateinit var genres: List<Genre>
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)"

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Song>
val albums: Collection<Album>
val artists: Collection<Artist>
val genres: Collection<Genre>
val playlists: Collection<Playlist>
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<Song>): MutableLibrary
suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): 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<Song>): MutableLibrary {
TODO("Not yet implemented")
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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.

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
)
data class PrePlaylist(val name: Name.Known, val rawName: String?, val handle: PlaylistHandle)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<AudioFile>, interpretation: Interpretation): Flow<PreSong>
}
class PreparerImpl(
private val nameFactory: Name.Known.Factory,
private val separators: Separators
) : Preparer {
override fun prepare(audioFiles: Flow<AudioFile>, 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<AudioFile>, 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 <T> 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<PreArtist>,
albumPreArtists: List<PreArtist>
albumPreArtists: List<PreArtist>,
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<String>,
rawNames: List<String>,
rawSortNames: List<String>
rawSortNames: List<String>,
interpretation: Interpretation
): List<PreArtist> {
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<PreGenre> {
private fun makePreGenres(
audioFile: AudioFile,
interpretation: Interpretation
): List<PreGenre> {
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)
}
private fun unknownPreGenre() = PreGenre(Name.Unknown(R.string.def_genre), null)
}

View file

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

View file

@ -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<QueueHeapItem>
val mappingItems: List<QueueShuffledMappingItem>
@ -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 }

View file

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

View file

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

View file

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

View file

@ -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<Item> {
private suspend fun searchImpl(library: Library, query: String): List<Item> {
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)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<TagDao> {
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<TagDao> {
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<TagDao> { coEvery { readSongs() } throws IllegalStateException() }
val cacheRepository = CacheRepositoryImpl(dao)
assertEquals(null, runBlocking { cacheRepository.readCache() })
coVerifyAll { dao.readSongs() }
}
@Test
fun cache_write() {
var currentlyStoredSongs = listOf<Tags>()
val insertSongsArg = slot<List<Tags>>()
val dao =
mockk<TagDao> {
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<TagDao> {
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<TagDao> {
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"),
)
}
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<SongImpl> {
every { uid } returns songUidA
every { durationMs } returns 0
every { path } returns Path(mockk(), Components.parseUnix("./"))
every { finalize() } returns this
}
val songB =
mockk<SongImpl> {
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<AlbumImpl> {
every { uid } returns albumUidA
every { durationMs } returns 0
every { finalize() } returns this
}
val albumB =
mockk<AlbumImpl> {
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<ArtistImpl> {
every { uid } returns artistUidA
every { durationMs } returns 0
every { finalize() } returns this
}
val artistB =
mockk<ArtistImpl> {
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<GenreImpl> {
every { uid } returns genreUidA
every { durationMs } returns 0
every { finalize() } returns this
}
val genreB =
mockk<GenreImpl> {
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<SongImpl> {
every { uid } returns Music.UID.auxio(MusicType.SONGS)
every { path } returns Path(mockk(), Components.parseUnix("./"))
every { finalize() } returns this
}
val songB =
mockk<SongImpl> {
every { uid } returns Music.UID.auxio(MusicType.SONGS)
every { path } returns Path(mockk(), Components.parseUnix("./"))
every { finalize() } returns this
}
val album =
mockk<AlbumImpl> {
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())
}
}