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>? { 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 songs = listSettings.albumSongSort.songs(album.songs)
val discs = songs.groupBy { it.disc } val discs = songs.groupBy { it.disc }
val section = val section =
@ -134,7 +134,7 @@ private class DetailGeneratorImpl(
} }
override fun artist(uid: Music.UID): Detail<Artist>? { 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 = val grouping =
artist.explicitAlbums.groupByTo(sortedMapOf()) { artist.explicitAlbums.groupByTo(sortedMapOf()) {
// Remap the complicated ReleaseType data structure into detail sections // Remap the complicated ReleaseType data structure into detail sections
@ -173,14 +173,14 @@ private class DetailGeneratorImpl(
} }
override fun genre(uid: Music.UID): Detail<Genre>? { 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 artists = DetailSection.Artists(GENRE_ARTIST_SORT.artists(genre.artists))
val songs = DetailSection.Songs(listSettings.genreSongSort.songs(genre.songs)) val songs = DetailSection.Songs(listSettings.genreSongSort.songs(genre.songs))
return Detail(genre, listOf(artists, songs)) return Detail(genre, listOf(artists, songs))
} }
override fun playlist(uid: Music.UID): Detail<Playlist>? { 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()) { if (playlist.songs.isNotEmpty()) {
val songs = DetailSection.Songs(playlist.songs) val songs = DetailSection.Songs(playlist.songs)
return Detail(playlist, listOf(songs)) return Detail(playlist, listOf(songs))

View file

@ -315,7 +315,7 @@ constructor(
*/ */
fun setSong(uid: Music.UID) { fun setSong(uid: Music.UID) {
L.d("Opening song $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) { if (_currentSong.value == null) {
L.w("Given song UID was invalid") L.w("Given song UID was invalid")
} }

View file

@ -25,10 +25,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.model.DeviceLibrary
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -56,9 +56,9 @@ class DetailPickerViewModel @Inject constructor(private val musicRepository: Mus
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return 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. // 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}") L.d("Updated artist choices: ${_artistChoices.value}")
} }
@ -99,15 +99,14 @@ sealed interface ArtistShowChoices {
/** The current [Artist] choices. */ /** The current [Artist] choices. */
val choices: List<Artist> val choices: List<Artist>
/** Sanitize this instance with a [DeviceLibrary]. */ /** 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]. */ /** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */
class FromSong(val song: Song) : ArtistShowChoices { class FromSong(val song: Song) : ArtistShowChoices {
override val uid = song.uid override val uid = song.uid
override val choices = song.artists override val choices = song.artists
override fun sanitize(newLibrary: DeviceLibrary) = override fun sanitize(newLibrary: Library) = newLibrary.findSong(uid)?.let { FromSong(it) }
newLibrary.findSong(uid)?.let { FromSong(it) }
} }
/** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */ /** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */
@ -115,7 +114,7 @@ sealed interface ArtistShowChoices {
override val uid = album.uid override val uid = album.uid
override val choices = album.artists override val choices = album.artists
override fun sanitize(newLibrary: DeviceLibrary) = override fun sanitize(newLibrary: Library) =
newLibrary.findAlbum(uid)?.let { FromAlbum(it) } newLibrary.findAlbum(uid)?.let { FromAlbum(it) }
} }
} }

View file

@ -119,8 +119,8 @@ private class HomeGeneratorImpl(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary val library = musicRepository.library
if (changes.deviceLibrary && deviceLibrary != null) { if (changes.deviceLibrary && library != null) {
L.d("Refreshing library") L.d("Refreshing library")
// Get the each list of items in the library to use as our list data. // Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them. // Applying the preferred sorting to them.
@ -130,8 +130,7 @@ private class HomeGeneratorImpl(
invalidator.invalidateMusic(MusicType.GENRES, UpdateInstructions.Diff) invalidator.invalidateMusic(MusicType.GENRES, UpdateInstructions.Diff)
} }
val userLibrary = musicRepository.userLibrary if (changes.userLibrary && library != null) {
if (changes.userLibrary && userLibrary != null) {
L.d("Refreshing playlists") L.d("Refreshing playlists")
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff) invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff)
} }
@ -144,14 +143,13 @@ private class HomeGeneratorImpl(
} }
override fun songs() = override fun songs() =
musicRepository.deviceLibrary?.let { listSettings.songSort.songs(it.songs) } ?: emptyList() musicRepository.library?.let { listSettings.songSort.songs(it.songs) } ?: emptyList()
override fun albums() = override fun albums() =
musicRepository.deviceLibrary?.let { listSettings.albumSort.albums(it.albums) } musicRepository.library?.let { listSettings.albumSort.albums(it.albums) } ?: emptyList()
?: emptyList()
override fun artists() = override fun artists() =
musicRepository.deviceLibrary?.let { deviceLibrary -> musicRepository.library?.let { deviceLibrary ->
val sorted = listSettings.artistSort.artists(deviceLibrary.artists) val sorted = listSettings.artistSort.artists(deviceLibrary.artists)
if (homeSettings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
sorted.filter { it.explicitAlbums.isNotEmpty() } sorted.filter { it.explicitAlbums.isNotEmpty() }
@ -161,11 +159,10 @@ private class HomeGeneratorImpl(
} ?: emptyList() } ?: emptyList()
override fun genres() = override fun genres() =
musicRepository.deviceLibrary?.let { listSettings.genreSort.genres(it.genres) } musicRepository.library?.let { listSettings.genreSort.genres(it.genres) } ?: emptyList()
?: emptyList()
override fun playlists() = override fun playlists() =
musicRepository.userLibrary?.let { listSettings.playlistSort.playlists(it.playlists) } musicRepository.library?.let { listSettings.playlistSort.playlists(it.playlists) }
?: emptyList() ?: emptyList()
override fun tabs() = homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type } 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) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
val userLibrary = musicRepository.userLibrary ?: return
// Sanitize the selection to remove items that no longer exist and thus // Sanitize the selection to remove items that no longer exist and thus
// won't appear in any list. // won't appear in any list.
_selected.value = _selected.value =
_selected.value.mapNotNull { _selected.value.mapNotNull {
when (it) { when (it) {
is Song -> deviceLibrary.findSong(it.uid) is Song -> library.findSong(it.uid)
is Album -> deviceLibrary.findAlbum(it.uid) is Album -> library.findAlbum(it.uid)
is Artist -> deviceLibrary.findArtist(it.uid) is Artist -> library.findArtist(it.uid)
is Genre -> deviceLibrary.findGenre(it.uid) is Genre -> library.findGenre(it.uid)
is Playlist -> userLibrary.findPlaylist(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? { 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 parent = parcel.playWithUid?.let(musicRepository::find) as MusicParent?
val playWith = PlaySong.fromIntCode(parcel.playWithCode, parent) ?: return null val playWith = PlaySong.fromIntCode(parcel.playWithCode, parent) ?: return null
return Menu.ForSong(parcel.res, song, playWith) return Menu.ForSong(parcel.res, song, playWith)
} }
private fun unpackAlbumParcel(parcel: Menu.ForAlbum.Parcel): Menu.ForAlbum? { 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) return Menu.ForAlbum(parcel.res, album)
} }
private fun unpackArtistParcel(parcel: Menu.ForArtist.Parcel): Menu.ForArtist? { 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) return Menu.ForArtist(parcel.res, artist)
} }
private fun unpackGenreParcel(parcel: Menu.ForGenre.Parcel): Menu.ForGenre? { 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) return Menu.ForGenre(parcel.res, genre)
} }
private fun unpackPlaylistParcel(parcel: Menu.ForPlaylist.Parcel): Menu.ForPlaylist? { 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) return Menu.ForPlaylist(parcel.res, playlist)
} }
private fun unpackSelectionParcel(parcel: Menu.ForSelection.Parcel): Menu.ForSelection? { private fun unpackSelectionParcel(parcel: Menu.ForSelection.Parcel): Menu.ForSelection? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null val library = musicRepository.library ?: return null
val songs = parcel.songUids.mapNotNull(deviceLibrary::findSong) val songs = parcel.songUids.mapNotNull(library::findSong)
return Menu.ForSelection(parcel.res, songs) 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.concatLocalized
import org.oxycblt.auxio.util.toUuidOrNull 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 * Abstract music data. This contains universal information about all concrete music
* implementations, such as identification information and names. * implementations, such as identification information and names.

View file

@ -29,12 +29,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.model.DeviceLibrary
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.Indexer import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.stack.interpret.Interpretation
import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -49,10 +48,7 @@ import timber.log.Timber as L
* configurations * configurations
*/ */
interface MusicRepository { interface MusicRepository {
/** The current music information found on the device. */ val library: Library?
val deviceLibrary: DeviceLibrary?
/** The current user-defined music information. */
val userLibrary: UserLibrary?
/** The current state of music loading. Null if no load has occurred yet. */ /** The current state of music loading. Null if no load has occurred yet. */
val indexingState: IndexingState? val indexingState: IndexingState?
@ -182,7 +178,7 @@ interface MusicRepository {
* Flags indicating which kinds of music information changed. * Flags indicating which kinds of music information changed.
* *
* @param deviceLibrary Whether the current [DeviceLibrary] has 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) data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean)
@ -212,18 +208,13 @@ interface MusicRepository {
class MusicRepositoryImpl class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(private val indexer: Indexer, private val musicSettings: MusicSettings) :
private val indexer: Indexer, MusicRepository {
private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory,
private val musicSettings: MusicSettings
) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>() private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null @Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null
@Volatile override var deviceLibrary: DeviceLibrary? = null @Volatile override var library: MutableLibrary? = null
@Volatile override var userLibrary: MutableUserLibrary? = null
@Volatile private var previousCompletedState: IndexingState.Completed? = null @Volatile private var previousCompletedState: IndexingState.Completed? = null
@Volatile private var currentIndexingState: IndexingState? = null @Volatile private var currentIndexingState: IndexingState? = null
override val indexingState: IndexingState? override val indexingState: IndexingState?
@ -282,41 +273,50 @@ constructor(
@Synchronized @Synchronized
override fun find(uid: Music.UID) = override fun find(uid: Music.UID) =
(deviceLibrary?.run { findSong(uid) ?: findAlbum(uid) ?: findArtist(uid) ?: findGenre(uid) } (library?.run {
?: userLibrary?.findPlaylist(uid)) findSong(uid)
?: findAlbum(uid)
?: findArtist(uid)
?: findGenre(uid)
?: findPlaylist(uid)
})
override suspend fun createPlaylist(name: String, songs: List<Song>) { 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") 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) } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) }
} }
override suspend fun renamePlaylist(playlist: Playlist, name: String) { 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") 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) } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) }
} }
override suspend fun deletePlaylist(playlist: Playlist) { override suspend fun deletePlaylist(playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return } val library = synchronized(this) { library ?: return }
L.d("Deleting $playlist") 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) } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) }
} }
override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) { 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") 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) } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) }
} }
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) { 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") L.d("Rewriting $playlist with ${songs.size} songs")
userLibrary.rewritePlaylist(playlist, songs) library.rewritePlaylist(playlist, songs)
withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) }
} }
@ -363,25 +363,28 @@ constructor(
Name.Known.SimpleFactory 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 // We want to make sure that all reads and writes are synchronized due to the sheer
// amount of consumers of MusicRepository. // amount of consumers of MusicRepository.
// TODO: Would Atomics not be a better fit here? // TODO: Would Atomics not be a better fit here?
val deviceLibraryChanged: Boolean
val userLibraryChanged: Boolean
synchronized(this) { synchronized(this) {
// It's possible that this reload might have changed nothing, so make sure that // It's possible that this reload might have changed nothing, so make sure that
// hasn't happened before dispatching a change to all consumers. // hasn't happened before dispatching a change to all consumers.
deviceLibraryChanged = this.deviceLibrary != deviceLibrary deviceLibraryChanged =
userLibraryChanged = this.userLibrary != userLibrary 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) { if (!deviceLibraryChanged && !userLibraryChanged) {
L.d("Library has not changed, skipping update") L.d("Library has not changed, skipping update")
return return
} }
this.deviceLibrary = deviceLibrary this.library = newLibrary
this.userLibrary = userLibrary
} }
// Consumers expect their updates to be on the main thread (notably PlaybackService), // 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) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
_statistics.value = _statistics.value =
Statistics( Statistics(
deviceLibrary.songs.size, library.songs.size,
deviceLibrary.albums.size, library.albums.size,
deviceLibrary.artists.size, library.artists.size,
deviceLibrary.genres.size, library.genres.size,
deviceLibrary.songs.sumOf { it.durationMs }) library.songs.sumOf { it.durationMs })
L.d("Updated statistics: ${_statistics.value}") L.d("Updated statistics: ${_statistics.value}")
} }
@ -162,10 +162,10 @@ constructor(
return@launch return@launch
} }
val deviceLibrary = musicRepository.deviceLibrary ?: return@launch val library = musicRepository.library ?: return@launch
val songs = val songs =
importedPlaylist.paths.mapNotNull { importedPlaylist.paths.mapNotNull {
it.firstNotNullOfOrNull(deviceLibrary::findSongByPath) it.firstNotNullOfOrNull(library::findSongByPath)
} }
if (songs.isEmpty()) { if (songs.isEmpty()) {

View file

@ -89,13 +89,13 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
var refreshChoicesWith: List<Song>? = null var refreshChoicesWith: List<Song>? = null
val deviceLibrary = musicRepository.deviceLibrary val library = musicRepository.library
if (changes.deviceLibrary && deviceLibrary != null) { if (changes.deviceLibrary && library != null) {
_currentPendingNewPlaylist.value = _currentPendingNewPlaylist.value =
_currentPendingNewPlaylist.value?.let { pendingPlaylist -> _currentPendingNewPlaylist.value?.let { pendingPlaylist ->
PendingNewPlaylist( PendingNewPlaylist(
pendingPlaylist.preferredName, pendingPlaylist.preferredName,
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) }, pendingPlaylist.songs.mapNotNull { library.findSong(it.uid) },
pendingPlaylist.template, pendingPlaylist.template,
pendingPlaylist.reason) pendingPlaylist.reason)
} }
@ -104,7 +104,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
_currentSongsToAdd.value = _currentSongsToAdd.value =
_currentSongsToAdd.value?.let { pendingSongs -> _currentSongsToAdd.value?.let { pendingSongs ->
pendingSongs pendingSongs
.mapNotNull { deviceLibrary.findSong(it.uid) } .mapNotNull { library.findSong(it.uid) }
.ifEmpty { null } .ifEmpty { null }
.also { refreshChoicesWith = it } .also { refreshChoicesWith = it }
} }
@ -127,7 +127,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
_currentPlaylistToExport.value = _currentPlaylistToExport.value =
_currentPlaylistToExport.value?.let { playlist -> _currentPlaylistToExport.value?.let { playlist ->
musicRepository.userLibrary?.findPlaylist(playlist.uid) musicRepository.library?.findPlaylist(playlist.uid)
} }
L.d("Updated playlist to export to ${_currentPlaylistToExport.value}") 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 reason: PlaylistDecision.New.Reason
) { ) {
L.d("Opening ${songUids.size} songs to create a playlist from") L.d("Opening ${songUids.size} songs to create a playlist from")
val userLibrary = musicRepository.userLibrary ?: return val library = musicRepository.library ?: return
val songs = val songs =
musicRepository.deviceLibrary musicRepository.library
?.let { songUids.mapNotNull(it::findSong) } ?.let { songUids.mapNotNull(it::findSong) }
?.also(::refreshPlaylistChoices) ?.also(::refreshPlaylistChoices)
val possibleName = val possibleName =
musicRepository.userLibrary?.let { musicRepository.library?.let {
// Attempt to generate a unique default name for the playlist, like "Playlist 1". // Attempt to generate a unique default name for the playlist, like "Playlist 1".
var i = 1 var i = 1
var possibleName: String var possibleName: String
@ -168,7 +168,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
possibleName = context.getString(R.string.fmt_def_playlist, i) possibleName = context.getString(R.string.fmt_def_playlist, i)
L.d("Trying $possibleName as a playlist name") L.d("Trying $possibleName as a playlist name")
++i ++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") L.d("$possibleName is unique, using it as the playlist name")
possibleName possibleName
} }
@ -194,9 +194,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
reason: PlaylistDecision.Rename.Reason reason: PlaylistDecision.Rename.Reason
) { ) {
L.d("Opening playlist $playlistUid to rename") L.d("Opening playlist $playlistUid to rename")
val playlist = musicRepository.userLibrary?.findPlaylist(playlistUid) val playlist = musicRepository.library?.findPlaylist(playlistUid)
val applySongs = val applySongs = musicRepository.library?.let { applySongUids.mapNotNull(it::findSong) }
musicRepository.deviceLibrary?.let { applySongUids.mapNotNull(it::findSong) }
_currentPendingRenamePlaylist.value = _currentPendingRenamePlaylist.value =
if (playlist != null && applySongs != null) { if (playlist != null && applySongs != null) {
@ -216,7 +215,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
L.d("Opening playlist $playlistUid to export") L.d("Opening playlist $playlistUid to export")
// TODO: Add this guard to the rest of the methods here // TODO: Add this guard to the rest of the methods here
if (_currentPlaylistToExport.value?.uid == playlistUid) return if (_currentPlaylistToExport.value?.uid == playlistUid) return
_currentPlaylistToExport.value = musicRepository.userLibrary?.findPlaylist(playlistUid) _currentPlaylistToExport.value = musicRepository.library?.findPlaylist(playlistUid)
if (_currentPlaylistToExport.value == null) { if (_currentPlaylistToExport.value == null) {
L.w("Given playlist UID to export was invalid") L.w("Given playlist UID to export was invalid")
} else { } else {
@ -241,7 +240,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
*/ */
fun setPlaylistToDelete(playlistUid: Music.UID) { fun setPlaylistToDelete(playlistUid: Music.UID) {
L.d("Opening playlist $playlistUid to delete") L.d("Opening playlist $playlistUid to delete")
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) _currentPlaylistToDelete.value = musicRepository.library?.findPlaylist(playlistUid)
if (_currentPlaylistToDelete.value == null) { if (_currentPlaylistToDelete.value == null) {
L.w("Given playlist UID to delete was invalid") L.w("Given playlist UID to delete was invalid")
} }
@ -266,8 +265,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
} }
else -> { else -> {
val trimmed = name.trim() val trimmed = name.trim()
val userLibrary = musicRepository.userLibrary val library = musicRepository.library
if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { if (library != null && library.findPlaylistByName(trimmed) == null) {
L.d("Chosen name is valid") L.d("Chosen name is valid")
ChosenName.Valid(trimmed) ChosenName.Valid(trimmed)
} else { } else {
@ -286,7 +285,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
fun setSongsToAdd(songUids: Array<Music.UID>) { fun setSongsToAdd(songUids: Array<Music.UID>) {
L.d("Opening ${songUids.size} songs to add to a playlist") L.d("Opening ${songUids.size} songs to add to a playlist")
_currentSongsToAdd.value = _currentSongsToAdd.value =
musicRepository.deviceLibrary musicRepository.library
?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } } ?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } }
?.also(::refreshPlaylistChoices) ?.also(::refreshPlaylistChoices)
if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) { 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>) { private fun refreshPlaylistChoices(songs: List<Song>) {
val userLibrary = musicRepository.userLibrary ?: return val library = musicRepository.library ?: return
L.d("Refreshing playlist choices") L.d("Refreshing playlist choices")
_playlistAddChoices.value = _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() val songSet = it.songs.toSet()
PlaylistChoice(it, songs.all(songSet::contains)) PlaylistChoice(it, songs.all(songSet::contains))
} }

View file

@ -151,8 +151,7 @@ constructor(
else -> else ->
listOf( listOf(
InterpretedPath(Components.parseUnix(path), false), InterpretedPath(Components.parseUnix(path), false),
InterpretedPath(Components.parseWindows(path), true) InterpretedPath(Components.parseWindows(path), true))
)
} }
private fun expandInterpretation( 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. * Name.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify

View file

@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
return AudioProperties( return AudioProperties(
bitrate, bitrate,
sampleRate, 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 * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -146,7 +146,7 @@ private constructor(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
L.d("Music changed, updating shared objects") L.d("Music changed, updating shared objects")
// Wipe possibly-invalidated outdated covers // Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
@ -158,10 +158,7 @@ private constructor(
savedState.copy( savedState.copy(
parent = parent =
savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent? }, savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent? },
heap = heap = savedState.heap.map { song -> song?.let { library.findSong(it.uid) } }),
savedState.heap.map { song ->
song?.let { deviceLibrary.findSong(it.uid) }
}),
true) true)
} }
} }

View file

@ -95,14 +95,13 @@ private constructor(
} }
override fun invalidate(type: MusicType, replace: Int?) { override fun invalidate(type: MusicType, replace: Int?) {
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
val userLibrary = musicRepository.userLibrary ?: return
val music = val music =
when (type) { when (type) {
MusicType.ALBUMS -> deviceLibrary.albums MusicType.ALBUMS -> library.albums
MusicType.ARTISTS -> deviceLibrary.artists MusicType.ARTISTS -> library.artists
MusicType.GENRES -> deviceLibrary.genres MusicType.GENRES -> library.genres
MusicType.PLAYLISTS -> userLibrary.playlists MusicType.PLAYLISTS -> library.playlists
else -> return else -> return
} }
if (music.isEmpty()) { if (music.isEmpty()) {
@ -131,9 +130,7 @@ private constructor(
} }
fun getChildren(parentId: String, maxTabs: Int): List<MediaItem>? { fun getChildren(parentId: String, maxTabs: Int): List<MediaItem>? {
val deviceLibrary = musicRepository.deviceLibrary if (musicRepository.library == null) {
val userLibrary = musicRepository.userLibrary
if (deviceLibrary == null || userLibrary == null) {
return listOf() return listOf()
} }
return getMediaItemList(parentId, maxTabs) return getMediaItemList(parentId, maxTabs)
@ -143,15 +140,10 @@ private constructor(
if (query.isEmpty()) { if (query.isEmpty()) {
return mutableListOf() return mutableListOf()
} }
val deviceLibrary = musicRepository.deviceLibrary ?: return mutableListOf() val library = musicRepository.library ?: return mutableListOf()
val userLibrary = musicRepository.userLibrary ?: return mutableListOf()
val items = val items =
SearchEngine.Items( SearchEngine.Items(
deviceLibrary.songs, library.songs, library.albums, library.artists, library.genres, library.playlists)
deviceLibrary.albums,
deviceLibrary.artists,
deviceLibrary.genres,
userLibrary.playlists)
return searchEngine.search(items, query).toMediaItems() return searchEngine.search(items, query).toMediaItems()
} }

View file

@ -23,7 +23,6 @@ import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import org.oxycblt.auxio.music.stack.explore.Explorer import org.oxycblt.auxio.music.stack.explore.Explorer
import org.oxycblt.auxio.music.stack.interpret.Interpretation 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 import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
interface Indexer { interface Indexer {
suspend fun run( suspend fun run(uris: List<Uri>, interpretation: Interpretation): MutableLibrary
uris: List<Uri>,
interpretation: Interpretation
): MutableLibrary
} }
class IndexerImpl class IndexerImpl
@Inject @Inject
constructor( constructor(private val explorer: Explorer, private val interpreter: Interpreter) : Indexer {
private val explorer: Explorer, override suspend fun run(uris: List<Uri>, interpretation: Interpretation) = coroutineScope {
private val interpreter: Interpreter
) : Indexer {
override suspend fun run(
uris: List<Uri>,
interpretation: Interpretation
) = coroutineScope {
val files = explorer.explore(uris) val files = explorer.explore(uris)
val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer() val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer()
val playlistFiles = files.playlists.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 package org.oxycblt.auxio.music.stack.explore
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.stack.IndexerImpl
@Module @Module
@InstallIn(SingletonComponent::class) @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 package org.oxycblt.auxio.music.stack.explore
import android.net.Uri import android.net.Uri
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.withIndex 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.cache.TagCache
import org.oxycblt.auxio.music.stack.explore.extractor.TagExtractor 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.fs.DeviceFiles
import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists
import javax.inject.Inject
interface Explorer { interface Explorer {
fun explore(uris: List<Uri>): Files fun explore(uris: List<Uri>): Files
} }
data class Files( data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
val audios: Flow<AudioFile>,
val playlists: Flow<PlaylistFile>
)
class ExplorerImpl @Inject constructor( class ExplorerImpl
@Inject
constructor(
private val deviceFiles: DeviceFiles, private val deviceFiles: DeviceFiles,
private val tagCache: TagCache, private val tagCache: TagCache,
private val tagExtractor: TagExtractor, private val tagExtractor: TagExtractor,
@ -46,16 +60,20 @@ class ExplorerImpl @Inject constructor(
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer() val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer() val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
val (uncachedDeviceFiles, cachedAudioFiles) = tagRead.results() val (uncachedDeviceFiles, cachedAudioFiles) = tagRead.results()
val extractedAudioFiles = uncachedDeviceFiles.split(8).map { val extractedAudioFiles =
tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() uncachedDeviceFiles
}.asFlow().flattenMerge() .split(8)
.map { tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() }
.asFlow()
.flattenMerge()
val writtenAudioFiles = tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer() val writtenAudioFiles = tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer()
val playlistFiles = storedPlaylists.read() val playlistFiles = storedPlaylists.read()
return Files(merge(cachedAudioFiles, writtenAudioFiles), playlistFiles) return Files(merge(cachedAudioFiles, writtenAudioFiles), playlistFiles)
} }
private fun Flow<CacheResult>.results(): Pair<Flow<DeviceFile>, Flow<AudioFile>> { 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 files = shared.filterIsInstance<CacheResult.Miss>().map { it.deviceFile }
val songs = shared.filterIsInstance<CacheResult.Hit>().map { it.audioFile } val songs = shared.filterIsInstance<CacheResult.Hit>().map { it.audioFile }
return files to songs return files to songs
@ -63,10 +81,9 @@ class ExplorerImpl @Inject constructor(
private fun <T> Flow<T>.split(n: Int): Array<Flow<T>> { private fun <T> Flow<T>.split(n: Int): Array<Flow<T>> {
val indexed = withIndex() val indexed = withIndex()
val shared = indexed.shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) val shared =
return Array(n) { indexed.shareIn(
shared.filter { it.index % n == 0 } CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0)
.map { it.value } return Array(n) { shared.filter { it.index % n == 0 }.map { it.value } }
}
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,9 +21,9 @@ package org.oxycblt.auxio.music.stack.explore
import android.net.Uri import android.net.Uri
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song 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.info.Date
import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.music.stack.explore.fs.Path
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
data class DeviceFile( data class DeviceFile(
val uri: Uri, val uri: Uri,
@ -71,13 +71,17 @@ data class PlaylistFile(
interface PlaylistHandle { interface PlaylistHandle {
val uid: Music.UID val uid: Music.UID
suspend fun rename(name: String) suspend fun rename(name: String)
suspend fun add(songs: List<Song>) suspend fun add(songs: List<Song>)
suspend fun rewrite(songs: List<Song>) suspend fun rewrite(songs: List<Song>)
suspend fun delete() suspend fun delete()
} }
sealed interface SongPointer { sealed interface SongPointer {
data class UID(val uid: Music.UID) : 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 { sealed interface CacheResult {
data class Hit(val audioFile: AudioFile) : CacheResult data class Hit(val audioFile: AudioFile) : CacheResult
data class Miss(val deviceFile: DeviceFile) : CacheResult data class Miss(val deviceFile: DeviceFile) : CacheResult
} }
interface TagCache { interface TagCache {
fun read(files: Flow<DeviceFile>): Flow<CacheResult> fun read(files: Flow<DeviceFile>): Flow<CacheResult>

View file

@ -28,8 +28,8 @@ import androidx.room.Query
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.auxio.music.stack.explore.AudioFile
import org.oxycblt.auxio.music.info.Date 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.DeviceFile
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
@ -51,8 +51,8 @@ interface TagDao {
@TypeConverters(Tags.Converters::class) @TypeConverters(Tags.Converters::class)
data class Tags( data class Tags(
/** /**
* The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black box * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black
* only used for comparison. * box only used for comparison.
*/ */
@PrimaryKey val uri: String, @PrimaryKey val uri: String,
/** The latest date the [AudioFile]'s audio file was modified, as a unix epoch timestamp. */ /** 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 * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.stack.explore.extractor package org.oxycblt.auxio.music.stack.explore.extractor
import android.content.Context import android.content.Context
@ -26,33 +26,29 @@ import androidx.media3.exoplayer.MetadataRetriever
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.source.TrackGroupArray
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.guava.asDeferred import kotlinx.coroutines.guava.asDeferred
import javax.inject.Inject
import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.AudioFile
import org.oxycblt.auxio.music.stack.explore.DeviceFile import org.oxycblt.auxio.music.stack.explore.DeviceFile
interface TagExtractor { interface TagExtractor {
fun extract( fun extract(deviceFiles: Flow<DeviceFile>): Flow<AudioFile>
deviceFiles: Flow<DeviceFile>
): Flow<AudioFile>
} }
class TagExtractorImpl @Inject constructor( class TagExtractorImpl
@Inject
constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val mediaSourceFactory: MediaSource.Factory, private val mediaSourceFactory: MediaSource.Factory,
) : TagExtractor { ) : TagExtractor {
override fun extract( override fun extract(deviceFiles: Flow<DeviceFile>) = flow {
deviceFiles: Flow<DeviceFile>
) = flow {
val thread = HandlerThread("TagExtractor:${hashCode()}") val thread = HandlerThread("TagExtractor:${hashCode()}")
deviceFiles.collect { deviceFile -> deviceFiles.collect { deviceFile ->
val exoPlayerMetadataFuture = val exoPlayerMetadataFuture =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
mediaSourceFactory, MediaItem.fromUri(deviceFile.uri), thread mediaSourceFactory, MediaItem.fromUri(deviceFile.uri), thread)
)
val mediaMetadataRetriever = MediaMetadataRetriever() val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(context, deviceFile.uri) mediaMetadataRetriever.setDataSource(context, deviceFile.uri)
val exoPlayerMetadata = exoPlayerMetadataFuture.asDeferred().await() val exoPlayerMetadata = exoPlayerMetadataFuture.asDeferred().await()
@ -75,9 +71,12 @@ class TagExtractorImpl @Inject constructor(
val textTags = TextTags(metadata) val textTags = TextTags(metadata)
return AudioFile( return AudioFile(
deviceFile = input, deviceFile = input,
durationMs = need(retriever.extractMetadata( durationMs =
MediaMetadataRetriever.METADATA_KEY_DURATION need(
)?.toLong(), "duration"), retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong(),
"duration"),
replayGainTrackAdjustment = textTags.replayGainTrackAdjustment(), replayGainTrackAdjustment = textTags.replayGainTrackAdjustment(),
replayGainAlbumAdjustment = textTags.replayGainAlbumAdjustment(), replayGainAlbumAdjustment = textTags.replayGainAlbumAdjustment(),
musicBrainzId = textTags.musicBrainzId(), musicBrainzId = textTags.musicBrainzId(),
@ -97,15 +96,22 @@ class TagExtractorImpl @Inject constructor(
albumArtistMusicBrainzIds = textTags.albumArtistMusicBrainzIds() ?: listOf(), albumArtistMusicBrainzIds = textTags.albumArtistMusicBrainzIds() ?: listOf(),
albumArtistNames = textTags.albumArtistNames() ?: listOf(), albumArtistNames = textTags.albumArtistNames() ?: listOf(),
albumArtistSortNames = textTags.albumArtistSortNames() ?: 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( AudioFile(
deviceFile, deviceFile,
name = need(deviceFile.path.name, "name"), 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) = 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 package org.oxycblt.auxio.music.stack.explore.extractor
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
@ -6,28 +24,31 @@ import org.oxycblt.auxio.util.nonZeroOrNull
// Song // Song
fun TextTags.musicBrainzId() = fun TextTags.musicBrainzId() =
(vorbis["musicbrainz_releasetrackid"] ?: vorbis["musicbrainz release track id"] (vorbis["musicbrainz_releasetrackid"]
?: id3v2["TXXX:musicbrainz release track id"] ?: vorbis["musicbrainz release track id"]
?: id3v2["TXXX:musicbrainz_releasetrackid"])?.first() ?: id3v2["TXXX:musicbrainz release track id"]
?: id3v2["TXXX:musicbrainz_releasetrackid"])
?.first()
fun TextTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first() fun TextTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first()
fun TextTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first() fun TextTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first()
// Track. // Track.
fun TextTags.track() = (parseVorbisPositionField( fun TextTags.track() =
vorbis["tracknumber"]?.first(), (parseVorbisPositionField(
(vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first() vorbis["tracknumber"]?.first(),
) (vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first())
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() }) ?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
// Disc and it's subtitle name. // Disc and it's subtitle name.
fun TextTags.disc() = (parseVorbisPositionField( fun TextTags.disc() =
vorbis["discnumber"]?.first(), (parseVorbisPositionField(
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() }) vorbis["discnumber"]?.first(),
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() }) (vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
fun TextTags.subtitle() = fun TextTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
(vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
// Dates are somewhat complicated, as not only did their semantics change from a flat year // 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 // 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: Show original and normal dates side-by-side
// TODO: Handle dates that are in "January" because the actual specific release date // TODO: Handle dates that are in "January" because the actual specific release date
// isn't known? // isn't known?
fun TextTags.date() = (vorbis["originaldate"]?.run { Date.from(first()) } fun TextTags.date() =
?: vorbis["date"]?.run { Date.from(first()) } (vorbis["originaldate"]?.run { Date.from(first()) }
?: vorbis["year"]?.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 // Vorbis dates are less complicated, but there are still several types
// Our hierarchy for dates is as such: // Our hierarchy for dates is as such:
// 1. Original Date, as it solves the "Released in X, Remastered in Y" issue // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue
// 2. Date, as it is the most common date type // 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 // 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!) // date tag that android supports, so it must be 15 years old or more!)
id3v2["TDOR"]?.run { Date.from(first()) } id3v2["TDOR"]?.run { Date.from(first()) }
?: id3v2["TDRC"]?.run { Date.from(first()) } ?: id3v2["TDRC"]?.run { Date.from(first()) }
?: id3v2["TDRL"]?.run { Date.from(first()) } ?: id3v2["TDRL"]?.run { Date.from(first()) }
?: parseId3v23Date()) ?: parseId3v23Date())
// Album // Album
fun TextTags.albumMusicBrainzId() = fun TextTags.albumMusicBrainzId() =
(vorbis["musicbrainz_albumid"] ?: vorbis["musicbrainz album id"] (vorbis["musicbrainz_albumid"]
?: id3v2["TXXX:musicbrainz album id"] ?: id3v2["TXXX:musicbrainz_albumid"])?.first() ?: vorbis["musicbrainz album id"]
?: id3v2["TXXX:musicbrainz album id"]
?: id3v2["TXXX:musicbrainz_albumid"])
?.first()
fun TextTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first() fun TextTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first()
fun TextTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.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:musicbrainz album type"]
?: id3v2["TXXX:releasetype"] ?: id3v2["TXXX:releasetype"]
?: ?:
// This is a non-standard iTunes extension // This is a non-standard iTunes extension
id3v2["GRP1"] id3v2["GRP1"])
)
// Artist // Artist
fun TextTags.artistMusicBrainzIds() = fun TextTags.artistMusicBrainzIds() =
(vorbis["musicbrainz_artistid"] ?: vorbis["musicbrainz artist id"] (vorbis["musicbrainz_artistid"]
?: id3v2["TXXX:musicbrainz artist id"] ?: id3v2["TXXX: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"] fun TextTags.artistNames() =
?: id3v2["TPE1"] ?: id3v2["TXXX:artist"]) (vorbis["artists"]
?: vorbis["artist"]
?: id3v2["TXXX:artists"]
?: id3v2["TPE1"]
?: id3v2["TXXX:artist"])
fun TextTags.artistSortNames() = (vorbis["artistssort"] fun TextTags.artistSortNames() =
?: vorbis["artists_sort"] (vorbis["artistssort"]
?: vorbis["artists sort"] ?: vorbis["artists_sort"]
?: vorbis["artistsort"] ?: vorbis["artists sort"]
?: vorbis["artist sort"] ?: id3v2["TXXX:artistssort"] ?: vorbis["artistsort"]
?: id3v2["TXXX:artists_sort"] ?: vorbis["artist sort"]
?: id3v2["TXXX:artists sort"] ?: id3v2["TXXX:artistssort"]
?: id3v2["TSOP"] ?: id3v2["TXXX:artists_sort"]
?: id3v2["artistsort"] ?: id3v2["TXXX:artists sort"]
?: id3v2["TXXX:artist sort"] ?: id3v2["TSOP"]
) ?: id3v2["artistsort"]
?: id3v2["TXXX:artist sort"])
fun TextTags.albumArtistMusicBrainzIds() = ( fun TextTags.albumArtistMusicBrainzIds() =
vorbis["musicbrainz_albumartistid"] ?: vorbis["musicbrainz album artist id"] (vorbis["musicbrainz_albumartistid"]
?: vorbis["musicbrainz album artist id"]
?: id3v2["TXXX:musicbrainz album artist id"] ?: id3v2["TXXX:musicbrainz album artist id"]
?: id3v2["TXXX:musicbrainz_albumartistid"] ?: id3v2["TXXX:musicbrainz_albumartistid"])
)
fun TextTags.albumArtistNames() = ( fun TextTags.albumArtistNames() =
vorbis["albumartists"] (vorbis["albumartists"]
?: vorbis["album_artists"] ?: vorbis["album_artists"]
?: vorbis["album artists"] ?: vorbis["album artists"]
?: vorbis["albumartist"] ?: vorbis["albumartist"]
?: vorbis["album artist"] ?: vorbis["album artist"]
?: id3v2["TXXX:albumartists"] ?: id3v2["TXXX:albumartists"]
?: id3v2["TXXX:album_artists"] ?: id3v2["TXXX:album_artists"]
?: id3v2["TXXX:album artists"] ?: id3v2["TXXX:album artists"]
?: id3v2["TPE2"] ?: id3v2["TPE2"]
?: id3v2["TXXX:albumartist"] ?: id3v2["TXXX:albumartist"]
?: id3v2["TXXX:album artist"] ?: id3v2["TXXX:album artist"])
)
fun TextTags.albumArtistSortNames() = (vorbis["albumartistssort"] fun TextTags.albumArtistSortNames() =
?: vorbis["albumartists_sort"] (vorbis["albumartistssort"]
?: vorbis["albumartists sort"] ?: vorbis["albumartists_sort"]
?: vorbis["albumartistsort"] ?: vorbis["albumartists sort"]
?: vorbis["album artist sort"] ?: id3v2["TXXX:albumartistssort"] ?: vorbis["albumartistsort"]
?: id3v2["TXXX:albumartists_sort"] ?: vorbis["album artist sort"]
?: id3v2["TXXX:albumartists sort"] ?: id3v2["TXXX:albumartistssort"]
?: id3v2["TXXX:albumartistsort"] ?: id3v2["TXXX:albumartists_sort"]
// This is a non-standard iTunes extension ?: id3v2["TXXX:albumartists sort"]
?: id3v2["TSO2"] ?: id3v2["TXXX:albumartistsort"]
?: id3v2["TXXX:album artist sort"] // This is a non-standard iTunes extension
) ?: id3v2["TSO2"]
?: id3v2["TXXX:album artist sort"])
// Genre // Genre
fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"] fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
// Compilation Flag // Compilation Flag
fun TextTags.isCompilation() = (vorbis["compilation"] ?: vorbis["itunescompilation"] fun TextTags.isCompilation() =
?: id3v2["TCMP"] // This is a non-standard itunes extension (vorbis["compilation"]
?: id3v2["TXXX:compilation"] ?: id3v2["TXXX:itunescompilation"] ?: vorbis["itunescompilation"]
) ?: id3v2["TCMP"] // This is a non-standard itunes extension
?.let { ?: id3v2["TXXX:compilation"]
// Ignore invalid instances of this tag ?: id3v2["TXXX:itunescompilation"])
it == listOf("1") ?.let {
} // Ignore invalid instances of this tag
it == listOf("1")
}
// ReplayGain information // ReplayGain information
fun TextTags.replayGainTrackAdjustment() = (vorbis["r128_track_gain"]?.parseR128Adjustment() fun TextTags.replayGainTrackAdjustment() =
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment() (vorbis["r128_track_gain"]?.parseR128Adjustment()
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()) ?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
fun TextTags.replayGainAlbumAdjustment() = (vorbis["r128_album_gain"]?.parseR128Adjustment() fun TextTags.replayGainAlbumAdjustment() =
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment() (vorbis["r128_album_gain"]?.parseR128Adjustment()
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()) ?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment())
private fun TextTags.parseId3v23Date(): Date? { private fun TextTags.parseId3v23Date(): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY // 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() = private fun List<String>.parseR128Adjustment() =
first() first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()?.run {
.replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "") // Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale
.toFloatOrNull() this / 256f + 5
?.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. * Parse a ReplayGain adjustment into a float value.
@ -199,7 +234,6 @@ private fun List<String>.parseR128Adjustment() =
private fun List<String>.parseReplayGainAdjustment() = private fun List<String>.parseReplayGainAdjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
val COMPILATION_RELEASE_TYPES = listOf("compilation") 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: * Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla * 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 * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -117,7 +117,6 @@ fun String.parseId3v2PositionField() =
fun parseVorbisPositionField(pos: String?, total: String?) = fun parseVorbisPositionField(pos: String?, total: String?) =
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull()) transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
/** /**
* Transform a raw position + total field into a position a way that tolerates placeholder values. * 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 pos
} else { } else {
null null
} }

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

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 package org.oxycblt.auxio.music.stack.explore.playlists
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.music.stack.explore.PlaylistFile import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.explore.SongPointer
import javax.inject.Inject
interface StoredPlaylists { interface StoredPlaylists {
fun read(): Flow<PlaylistFile> fun read(): Flow<PlaylistFile>
} }
class StoredPlaylistsImpl @Inject constructor( class StoredPlaylistsImpl @Inject constructor(private val playlistDao: PlaylistDao) :
private val playlistDao: PlaylistDao StoredPlaylists {
) : StoredPlaylists { override fun read() = flow { emitAll(playlistDao.readRawPlaylists().asFlow().map { TODO() }) }
override fun read() = flow { }
emitAll(playlistDao.readRawPlaylists()
.asFlow()
.map {
TODO()
})
}
}

View file

@ -26,5 +26,5 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface InterpretModule { 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 package org.oxycblt.auxio.music.stack.interpret
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
data class Interpretation( data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)
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 package org.oxycblt.auxio.music.stack.interpret
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
@ -30,17 +49,14 @@ interface Interpreter {
): MutableLibrary ): MutableLibrary
} }
class InterpreterImpl( class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Interpreter {
private val preparer: Preparer
) : Interpreter {
override suspend fun interpret( override suspend fun interpret(
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation interpretation: Interpretation
): MutableLibrary { ): MutableLibrary {
val preSongs = val preSongs =
preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main) preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main).buffer()
.buffer()
val genreLinker = GenreLinker() val genreLinker = GenreLinker()
val genreLinkedSongs = genreLinker.register(preSongs).flowOn(Dispatchers.Main).buffer() val genreLinkedSongs = genreLinker.register(preSongs).flowOn(Dispatchers.Main).buffer()
val artistLinker = ArtistLinker() val artistLinker = ArtistLinker()
@ -53,7 +69,8 @@ class InterpreterImpl(
val artists = artistLinker.resolve() val artists = artistLinker.resolve()
val albumLinker = AlbumLinker() val albumLinker = AlbumLinker()
val albumLinkedSongs = val albumLinkedSongs =
albumLinker.register(artistLinkedSongs) albumLinker
.register(artistLinkedSongs)
.flowOn(Dispatchers.Main) .flowOn(Dispatchers.Main)
.map { LinkedSongImpl(it) } .map { LinkedSongImpl(it) }
.toList() .toList()
@ -62,13 +79,18 @@ class InterpreterImpl(
return LibraryImpl(songs, albums, artists, genres) 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( override val album: Linked<AlbumImpl, SongImpl>
private val albumLinkedSong: AlbumLinker.LinkedSong get() = albumLinkedSong.album
) : LinkedSong {
override val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong override val artists: Linked<List<ArtistImpl>, SongImpl>
override val album: Linked<AlbumImpl, SongImpl> get() = albumLinkedSong.album get() = albumLinkedSong.linkedArtistSong.artists
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 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 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 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 { class AlbumLinker {
private val tree = mutableMapOf<String?, MutableMap<UUID?, AlbumLink>>() private val tree = mutableMapOf<String?, MutableMap<UUID?, AlbumLink>>()
fun register(linkedSongs: Flow<ArtistLinker.LinkedSong>) = linkedSongs.map { fun register(linkedSongs: Flow<ArtistLinker.LinkedSong>) =
val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase() linkedSongs.map {
val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase()
val albumLink = tree.getOrPut(nameKey) { mutableMapOf() } val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId
.getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) } val albumLink =
albumLink.node.contributors.contribute(it.linkedAlbum) tree
LinkedSong(it, albumLink) .getOrPut(nameKey) { mutableMapOf() }
} .getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) }
albumLink.node.contributors.contribute(it.linkedAlbum)
LinkedSong(it, albumLink)
}
fun resolve(): Collection<AlbumImpl> = fun resolve(): Collection<AlbumImpl> =
tree.values.flatMap { musicBrainzIdBundle -> tree.values.flatMap { musicBrainzIdBundle ->
val only = val only = musicBrainzIdBundle.values.singleOrNull()
musicBrainzIdBundle.values.singleOrNull()
if (only != null) { if (only != null) {
return@flatMap listOf(only.node.resolve()) return@flatMap listOf(only.node.resolve())
} }
val nullBundle = musicBrainzIdBundle[null] val nullBundle =
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } musicBrainzIdBundle[null]
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
// Only partially tagged with MBIDs, must go through and // Only partially tagged with MBIDs, must go through and
musicBrainzIdBundle.filter { it.key != null }.forEach { musicBrainzIdBundle
val candidates = it.value.node.contributors.candidates .filter { it.key != null }
nullBundle.node.contributors.contribute(candidates) .forEach {
it.value.node = nullBundle.node val candidates = it.value.node.contributors.candidates
} nullBundle.node.contributors.contribute(candidates)
it.value.node = nullBundle.node
}
listOf(nullBundle.node.resolve()) listOf(nullBundle.node.resolve())
} }
data class LinkedSong( data class LinkedSong(
val linkedSong: ArtistLinker.LinkedSong, val linkedArtistSong: ArtistLinker.LinkedSong,
val album: Linked<AlbumImpl, SongImpl> val album: Linked<AlbumImpl, SongImpl>
) )
private data class AlbumLink( private data class AlbumLink(var node: AlbumNode) : Linked<AlbumImpl, SongImpl> {
var node: AlbumNode
) : Linked<AlbumImpl, SongImpl> {
override fun resolve(child: SongImpl): AlbumImpl { override fun resolve(child: SongImpl): AlbumImpl {
return requireNotNull(node.albumImpl) { "Album not resolved yet" }.also { return requireNotNull(node.albumImpl) { "Album not resolved yet" }
it.link(child) .also { it.link(child) }
}
} }
} }
private class AlbumNode( private class AlbumNode(val contributors: Contribution<ArtistLinker.LinkedAlbum>) {
val contributors: Contribution<ArtistLinker.LinkedAlbum>
) {
var albumImpl: AlbumImpl? = null var albumImpl: AlbumImpl? = null
private set private set
@ -74,9 +82,8 @@ class AlbumLinker {
} }
} }
private class LinkedAlbumImpl( private class LinkedAlbumImpl(private val artistLinkedAlbum: ArtistLinker.LinkedAlbum) :
private val artistLinkedAlbum: ArtistLinker.LinkedAlbum LinkedAlbum {
) : LinkedAlbum {
override val preAlbum = artistLinkedAlbum.preAlbum override val preAlbum = artistLinkedAlbum.preAlbum
override val artists = artistLinkedAlbum.artists 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 package org.oxycblt.auxio.music.stack.interpret.linker
import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music 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.AlbumImpl
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl 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.model.SongImpl
import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum 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.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 { class ArtistLinker {
private val tree = mutableMapOf<String?, MutableMap<UUID?, ArtistLink>>() private val tree = mutableMapOf<String?, MutableMap<UUID?, ArtistLink>>()
fun register(linkedSongs: Flow<GenreLinker.LinkedSong>) = linkedSongs.map { fun register(linkedSongs: Flow<GenreLinker.LinkedSong>) =
val linkedSongArtists = it.preSong.preArtists.map { artist -> linkedSongs.map {
val nameKey = artist.rawName?.lowercase() val linkedSongArtists =
val musicBrainzIdKey = artist.musicBrainzId it.preSong.preArtists.map { artist ->
val artistLink = tree.getOrPut(nameKey) { mutableMapOf() } val nameKey = artist.rawName?.lowercase()
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) } val musicBrainzIdKey = artist.musicBrainzId
artistLink.node.contributors.contribute(artist) val artistLink =
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> = fun resolve(): Collection<ArtistImpl> =
tree.values.flatMap { musicBrainzIdBundle -> tree.values.flatMap { musicBrainzIdBundle ->
val only = val only = musicBrainzIdBundle.values.singleOrNull()
musicBrainzIdBundle.values.singleOrNull()
if (only != null) { if (only != null) {
return@flatMap listOf(only.node.resolve()) return@flatMap listOf(only.node.resolve())
} }
val nullBundle = musicBrainzIdBundle[null] val nullBundle =
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() } musicBrainzIdBundle[null]
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
// Only partially tagged with MBIDs, must go through and // Only partially tagged with MBIDs, must go through and
musicBrainzIdBundle.filter { it.key != null }.forEach { musicBrainzIdBundle
val candidates = it.value.node.contributors.candidates .filter { it.key != null }
nullBundle.node.contributors.contribute(candidates) .forEach {
it.value.node = nullBundle.node val candidates = it.value.node.contributors.candidates
} nullBundle.node.contributors.contribute(candidates)
it.value.node = nullBundle.node
}
listOf(nullBundle.node.resolve()) listOf(nullBundle.node.resolve())
} }
@ -71,31 +90,27 @@ class ArtistLinker {
val artists: Linked<List<ArtistImpl>, AlbumImpl> val artists: Linked<List<ArtistImpl>, AlbumImpl>
) )
private class MultiArtistLink<T : Music>( private class MultiArtistLink<T : Music>(val links: List<Linked<ArtistImpl, Music>>) :
val links: List<Linked<ArtistImpl, Music>> Linked<List<ArtistImpl>, T> {
) : Linked<List<ArtistImpl>, T> {
override fun resolve(child: T): List<ArtistImpl> { override fun resolve(child: T): List<ArtistImpl> {
return links.map { it.resolve(child) }.distinct() return links.map { it.resolve(child) }.distinct()
} }
} }
private data class ArtistLink( private data class ArtistLink(var node: ArtistNode) : Linked<ArtistImpl, Music> {
var node: ArtistNode
) : Linked<ArtistImpl, Music> {
override fun resolve(child: Music): ArtistImpl { override fun resolve(child: Music): ArtistImpl {
return requireNotNull(node.artistImpl) { "Artist not resolved yet" }.also { return requireNotNull(node.artistImpl) { "Artist not resolved yet" }
when (child) { .also {
is SongImpl -> it.link(child) when (child) {
is AlbumImpl -> it.link(child) is SongImpl -> it.link(child)
else -> error("Cannot link to child $child") is AlbumImpl -> it.link(child)
else -> error("Cannot link to child $child")
}
} }
}
} }
} }
private class ArtistNode( private class ArtistNode(val contributors: Contribution<PreArtist>) {
val contributors: Contribution<PreArtist>
) {
var artistImpl: ArtistImpl? = null var artistImpl: ArtistImpl? = null
private set 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 package org.oxycblt.auxio.music.stack.interpret.linker
class Contribution<T> { class Contribution<T> {
private val map = mutableMapOf<T, Int>() private val map = mutableMapOf<T, Int>()
val candidates: Collection<T> get() = map.keys val candidates: Collection<T>
get() = map.keys
fun contribute(key: T) { fun contribute(key: T) {
map[key] = map.getOrDefault(key, 0) + 1 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") 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 package org.oxycblt.auxio.music.stack.interpret.linker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map 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.GenreImpl
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre
@ -12,45 +28,37 @@ import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
class GenreLinker { class GenreLinker {
private val tree = mutableMapOf<String?, GenreLink>() private val tree = mutableMapOf<String?, GenreLink>()
fun register(preSong: Flow<PreSong>): Flow<LinkedSong> = preSong.map { fun register(preSong: Flow<PreSong>): Flow<LinkedSong> =
val genreLinks = it.preGenres.map { genre -> preSong.map {
val nameKey = genre.rawName?.lowercase() val genreLinks =
val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) } it.preGenres.map { genre ->
link.node.contributors.contribute(genre) val nameKey = genre.rawName?.lowercase()
link val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) }
link.node.contributors.contribute(genre)
link
}
LinkedSong(it, MultiGenreLink(genreLinks))
} }
LinkedSong(it, MultiGenreLink(genreLinks))
}
fun resolve() = fun resolve() = tree.values.map { it.node.resolve() }
tree.values.map { it.node.resolve() }
data class LinkedSong( data class LinkedSong(val preSong: PreSong, val genres: Linked<List<GenreImpl>, SongImpl>)
val preSong: PreSong,
val genres: Linked<List<GenreImpl>, SongImpl>
)
private class MultiGenreLink( private class MultiGenreLink(val links: List<Linked<GenreImpl, SongImpl>>) :
val links: List<Linked<GenreImpl, SongImpl>> Linked<List<GenreImpl>, SongImpl> {
) : Linked<List<GenreImpl>, SongImpl> {
override fun resolve(child: SongImpl): List<GenreImpl> { override fun resolve(child: SongImpl): List<GenreImpl> {
return links.map { it.resolve(child) }.distinct() return links.map { it.resolve(child) }.distinct()
} }
} }
private data class GenreLink( private data class GenreLink(var node: GenreNode) : Linked<GenreImpl, SongImpl> {
var node: GenreNode
) : Linked<GenreImpl, SongImpl> {
override fun resolve(child: SongImpl): GenreImpl { override fun resolve(child: SongImpl): GenreImpl {
return requireNotNull(node.genreImpl) { "Genre not resolved yet" }.also { return requireNotNull(node.genreImpl) { "Genre not resolved yet" }
it.link(child) .also { it.link(child) }
}
} }
} }
private class GenreNode( private class GenreNode(val contributors: Contribution<PreGenre>) {
val contributors: Contribution<PreGenre>
) {
var genreImpl: GenreImpl? = null var genreImpl: GenreImpl? = null
private set 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 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.AlbumImpl
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl 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.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 package org.oxycblt.auxio.music.stack.interpret.linker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.oxycblt.auxio.music.stack.explore.PlaylistFile 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.PlaylistImpl
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
class PlaylistLinker { 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() fun resolve(): Collection<PlaylistImpl> = setOf()
} }

View file

@ -15,9 +15,10 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.stack.interpret.model package org.oxycblt.auxio.music.stack.interpret.model
import kotlin.math.min
import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.image.extractor.ParentCover
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album 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.PreArtist
import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre
import org.oxycblt.auxio.util.update import org.oxycblt.auxio.util.update
import kotlin.math.min
/** /**
* Library-backed implementation of [Song]. * Library-backed implementation of [Song].
@ -81,9 +81,7 @@ class SongImpl(linkedSong: LinkedSong) : Song {
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is SongImpl && other is SongImpl && uid == other.uid && preSong == other.preSong
uid == other.uid &&
preSong == other.preSong
override fun toString() = "Song(uid=$uid, name=$name)" 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, // Since equality on public-facing music models is not identical to the tag equality,
// we just compare raw instances and how they are interpreted. // we just compare raw instances and how they are interpreted.
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is AlbumImpl && other is AlbumImpl && uid == other.uid && preAlbum == other.preAlbum && songs == other.songs
uid == other.uid &&
preAlbum == other.preAlbum &&
songs == other.songs
override fun toString() = "Album(uid=$uid, name=$name)" override fun toString() = "Album(uid=$uid, name=$name)"
@ -135,11 +130,11 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
durationMs += song.durationMs durationMs += song.durationMs
dateAdded = min(dateAdded, song.dateAdded) dateAdded = min(dateAdded, song.dateAdded)
if (song.date != null) { if (song.date != null) {
dates = dates?.let { dates =
if (song.date < it.min) Date.Range(song.date, it.max) dates?.let {
else if (song.date > it.max) Date.Range(it.min, song.date) if (song.date < it.min) Date.Range(song.date, it.max)
else it else if (song.date > it.max) Date.Range(it.min, song.date) else it
} ?: Date.Range(song.date, song.date) } ?: Date.Range(song.date, song.date)
} }
hashCode = 31 * hashCode + song.hashCode() 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 explicitAlbums: Set<Album>
override lateinit var implicitAlbums: Set<Album> override lateinit var implicitAlbums: Set<Album>
override lateinit var genres: List<Genre> override lateinit var genres: List<Genre>
override var durationMs = 0L 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. // we just compare raw instances and how they are interpreted.
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is ArtistImpl && other is ArtistImpl &&
uid == other.uid && uid == other.uid &&
preArtist == other.preArtist && preArtist == other.preArtist &&
songs == other.songs songs == other.songs
override fun toString() = "Artist(uid=$uid, name=$name)" override fun toString() = "Artist(uid=$uid, name=$name)"
@ -240,9 +234,7 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist {
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreImpl( class GenreImpl(private val preGenre: PreGenre) : Genre {
private val preGenre: PreGenre
) : Genre {
override val uid = Music.UID.auxio(MusicType.GENRES) { update(preGenre.rawName) } override val uid = Music.UID.auxio(MusicType.GENRES) { update(preGenre.rawName) }
override val name = preGenre.name override val name = preGenre.name
@ -256,10 +248,7 @@ class GenreImpl(
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is GenreImpl && other is GenreImpl && uid == other.uid && preGenre == other.preGenre && songs == other.songs
uid == other.uid &&
preGenre == other.preGenre &&
songs == other.songs
override fun toString() = "Genre(uid=$uid, name=$name)" 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 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
interface Library { import org.oxycblt.auxio.music.stack.explore.fs.Path
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?
}
interface MutableLibrary : Library { interface MutableLibrary : Library {
suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary
suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary
suspend fun deletePlaylist(playlist: Playlist): MutableLibrary suspend fun deletePlaylist(playlist: Playlist): MutableLibrary
} }
@ -41,6 +51,10 @@ class LibraryImpl(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun findSongByPath(path: Path): Song? {
TODO("Not yet implemented")
}
override fun findAlbum(uid: Music.UID): Album? { override fun findAlbum(uid: Music.UID): Album? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -57,6 +71,10 @@ class LibraryImpl(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun findPlaylistByName(name: String): Playlist? {
TODO("Not yet implemented")
}
override suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary { override suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View file

@ -19,13 +19,8 @@
package org.oxycblt.auxio.music.stack.interpret.model package org.oxycblt.auxio.music.stack.interpret.model
import org.oxycblt.auxio.image.extractor.ParentCover 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.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Name 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 import org.oxycblt.auxio.music.stack.interpret.linker.LinkedPlaylist
class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist { 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 package org.oxycblt.auxio.music.stack.interpret.prepare
/// --- ID3v2 PARSING --- /// --- ID3v2 PARSING ---
/** /**
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * 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 * 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. // try to index the genre table with such.
val numeric = val numeric =
toIntOrNull() toIntOrNull()
// Not a numeric value, try some other fixed values. // Not a numeric value, try some other fixed values.
?: return when (this) { ?: return when (this) {
// CR and RX are not technically ID3v1, but are formatted similarly to a plain // CR and RX are not technically ID3v1, but are formatted similarly to a plain
// number. // 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 package org.oxycblt.auxio.music.stack.interpret.prepare
import android.net.Uri import android.net.Uri
import java.util.UUID
import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.image.extractor.Cover
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType 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.PlaylistHandle
import org.oxycblt.auxio.music.stack.explore.fs.MimeType import org.oxycblt.auxio.music.stack.explore.fs.MimeType
import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.music.stack.explore.fs.Path
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import java.util.UUID
data class PreSong( data class PreSong(
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
@ -52,8 +69,4 @@ data class PreGenre(
val rawName: String?, val rawName: String?,
) )
data class PrePlaylist( data class PrePlaylist(val name: Name.Known, val rawName: String?, val handle: PlaylistHandle)
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 package org.oxycblt.auxio.music.stack.interpret.prepare
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.music.stack.interpret.Interpreter
import org.oxycblt.auxio.music.stack.interpret.InterpreterImpl
@Module @Module
@InstallIn(SingletonComponent::class) @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 package org.oxycblt.auxio.music.stack.interpret.prepare
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.R 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.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType 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.AudioFile
import org.oxycblt.auxio.music.stack.explore.fs.MimeType import org.oxycblt.auxio.music.stack.explore.fs.MimeType
import org.oxycblt.auxio.music.stack.interpret.Interpretation import org.oxycblt.auxio.music.stack.interpret.Interpretation
@ -18,128 +36,129 @@ interface Preparer {
fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong> fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
} }
class PreparerImpl( class PreparerImpl @Inject constructor() : Preparer {
private val nameFactory: Name.Known.Factory, override fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation) =
private val separators: Separators audioFiles.map { audioFile ->
) : Preparer { val individualPreArtists =
override fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation) = audioFiles.map { audioFile -> makePreArtists(
val individualPreArtists = makePreArtists( audioFile.artistMusicBrainzIds,
audioFile.artistMusicBrainzIds, audioFile.artistNames,
audioFile.artistNames, audioFile.artistSortNames,
audioFile.artistSortNames interpretation)
) val albumPreArtists =
val albumPreArtists = makePreArtists( makePreArtists(
audioFile.albumArtistMusicBrainzIds, audioFile.albumArtistMusicBrainzIds,
audioFile.albumArtistNames, audioFile.albumArtistNames,
audioFile.albumArtistSortNames audioFile.albumArtistSortNames,
) interpretation)
val preAlbum = makePreAlbum(audioFile, individualPreArtists, albumPreArtists) val preAlbum =
val rawArtists = makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation)
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } val rawArtists =
val rawGenres = individualPreArtists
makePreGenres(audioFile).ifEmpty { listOf(unknownPreGenre()) } .ifEmpty { albumPreArtists }
val uri = audioFile.deviceFile.uri .ifEmpty { listOf(unknownPreArtist()) }
PreSong( val rawGenres =
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(), makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) }
name = nameFactory.parse(need(audioFile, "name", audioFile.name), audioFile.sortName), val uri = audioFile.deviceFile.uri
rawName = audioFile.name, PreSong(
track = audioFile.track, musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) }, name =
date = audioFile.date, interpretation.nameFactory.parse(
uri = uri, need(audioFile, "name", audioFile.name), audioFile.sortName),
cover = inferCover(audioFile), rawName = audioFile.name,
path = need(audioFile, "path", audioFile.deviceFile.path), track = audioFile.track,
mimeType = MimeType( disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
need(audioFile, "mime type", audioFile.deviceFile.mimeType), date = audioFile.date,
null uri = uri,
), cover = inferCover(audioFile),
size = audioFile.deviceFile.size, path = need(audioFile, "path", audioFile.deviceFile.path),
durationMs = need(audioFile, "duration", audioFile.durationMs), mimeType =
replayGainAdjustment = ReplayGainAdjustment( MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null),
audioFile.replayGainTrackAdjustment, size = audioFile.deviceFile.size,
audioFile.replayGainAlbumAdjustment, durationMs = need(audioFile, "duration", audioFile.durationMs),
), replayGainAdjustment =
// TODO: Figure out what to do with date added ReplayGainAdjustment(
dateAdded = audioFile.deviceFile.lastModified, audioFile.replayGainTrackAdjustment,
preAlbum = preAlbum, audioFile.replayGainAlbumAdjustment,
preArtists = rawArtists, ),
preGenres = rawGenres // 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?) = private fun <T> need(audioFile: AudioFile, what: String, value: T?) =
requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" } requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" }
private fun inferCover(audioFile: AudioFile): Cover { private fun inferCover(audioFile: AudioFile): Cover {
return Cover.Embedded( return Cover.Embedded(audioFile.deviceFile.uri, audioFile.deviceFile.uri, "")
audioFile.deviceFile.uri,
audioFile.deviceFile.uri,
""
)
} }
private fun makePreAlbum( private fun makePreAlbum(
audioFile: AudioFile, audioFile: AudioFile,
individualPreArtists: List<PreArtist>, individualPreArtists: List<PreArtist>,
albumPreArtists: List<PreArtist> albumPreArtists: List<PreArtist>,
interpretation: Interpretation
): PreAlbum { ): PreAlbum {
val rawAlbumName = need(audioFile, "album name", audioFile.albumName) val rawAlbumName = need(audioFile, "album name", audioFile.albumName)
return PreAlbum( return PreAlbum(
musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(),
name = nameFactory.parse(rawAlbumName, audioFile.albumSortName), name = interpretation.nameFactory.parse(rawAlbumName, audioFile.albumSortName),
rawName = rawAlbumName, rawName = rawAlbumName,
releaseType = ReleaseType.parse(separators.split(audioFile.releaseTypes)) releaseType =
?: ReleaseType.Album(null), ReleaseType.parse(interpretation.separators.split(audioFile.releaseTypes))
?: ReleaseType.Album(null),
preArtists = preArtists =
albumPreArtists albumPreArtists
.ifEmpty { individualPreArtists } .ifEmpty { individualPreArtists }
.ifEmpty { listOf(unknownPreArtist()) }) .ifEmpty { listOf(unknownPreArtist()) })
} }
private fun makePreArtists( private fun makePreArtists(
rawMusicBrainzIds: List<String>, rawMusicBrainzIds: List<String>,
rawNames: List<String>, rawNames: List<String>,
rawSortNames: List<String> rawSortNames: List<String>,
interpretation: Interpretation
): List<PreArtist> { ): List<PreArtist> {
val musicBrainzIds = separators.split(rawMusicBrainzIds) val musicBrainzIds = interpretation.separators.split(rawMusicBrainzIds)
val names = separators.split(rawNames) val names = interpretation.separators.split(rawNames)
val sortNames = separators.split(rawSortNames) val sortNames = interpretation.separators.split(rawSortNames)
return names return names.mapIndexed { i, name ->
.mapIndexed { i, name -> makePreArtist(musicBrainzIds.getOrNull(i), name, sortNames.getOrNull(i), interpretation)
makePreArtist( }
musicBrainzIds.getOrNull(i),
name,
sortNames.getOrNull(i)
)
}
} }
private fun makePreArtist( private fun makePreArtist(
musicBrainzId: String?, musicBrainzId: String?,
rawName: String?, rawName: String?,
sortName: String? sortName: String?,
interpretation: Interpretation
): PreArtist { ): PreArtist {
val name = 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() val musicBrainzId = musicBrainzId?.toUuidOrNull()
return PreArtist(musicBrainzId, name, rawName) return PreArtist(musicBrainzId, name, rawName)
} }
private fun unknownPreArtist() = private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null)
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 = val genreNames =
audioFile.genreNames.parseId3GenreNames() ?: separators.split(audioFile.genreNames) audioFile.genreNames.parseId3GenreNames()
return genreNames.map { makePreGenre(it) } ?: interpretation.separators.split(audioFile.genreNames)
return genreNames.map { makePreGenre(it, interpretation) }
} }
private fun makePreGenre(rawName: String?) = private fun makePreGenre(rawName: String?, interpretation: Interpretation) =
PreGenre(rawName?.let { nameFactory.parse(it, null) } ?: Name.Unknown(R.string.def_genre), PreGenre(
rawName?.let { interpretation.nameFactory.parse(it, null) }
?: Name.Unknown(R.string.def_genre),
rawName) rawName)
private fun unknownPreGenre() = private fun unknownPreGenre() = PreGenre(Name.Unknown(R.string.def_genre), null)
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) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
_currentPickerSong.value = _currentPickerSong.value?.run { deviceLibrary.findSong(uid) } _currentPickerSong.value = _currentPickerSong.value?.run { library.findSong(uid) }
} }
override fun onCleared() { override fun onCleared() {
@ -64,7 +64,7 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M
*/ */
fun setPickerSongUid(uid: Music.UID) { fun setPickerSongUid(uid: Music.UID) {
L.d("Opening picker for song $uid") L.d("Opening picker for song $uid")
_currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) _currentPickerSong.value = musicRepository.library?.findSong(uid)
if (_currentPickerSong.value != null) { if (_currentPickerSong.value != null) {
L.w("Given song UID was invalid") L.w("Given song UID was invalid")
} }

View file

@ -50,7 +50,7 @@ constructor(
) : PersistenceRepository { ) : PersistenceRepository {
override suspend fun readState(): PlaybackStateManager.SavedState? { override suspend fun readState(): PlaybackStateManager.SavedState? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null val library = musicRepository.library ?: return null
val playbackState: PlaybackState val playbackState: PlaybackState
val heapItems: List<QueueHeapItem> val heapItems: List<QueueHeapItem>
val mappingItems: List<QueueShuffledMappingItem> val mappingItems: List<QueueShuffledMappingItem>
@ -64,7 +64,7 @@ constructor(
return null 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 shuffledMapping = mappingItems.map { it.index }
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }

View file

@ -130,8 +130,8 @@ class ExoPlaybackStateHolder(
get() = player.audioSessionId get() = player.audioSessionId
override fun resolveQueue(): RawQueue { override fun resolveQueue(): RawQueue {
val deviceLibrary = val library =
musicRepository.deviceLibrary musicRepository.library
// No library, cannot do anything. // No library, cannot do anything.
?: return RawQueue(emptyList(), emptyList(), 0) ?: return RawQueue(emptyList(), emptyList(), 0)
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) } val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) }
@ -145,8 +145,8 @@ class ExoPlaybackStateHolder(
} }
override fun handleDeferred(action: DeferredPlayback): Boolean { override fun handleDeferred(action: DeferredPlayback): Boolean {
val deviceLibrary = val library =
musicRepository.deviceLibrary musicRepository.library
// No library, cannot do anything. // No library, cannot do anything.
?: return false ?: 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 // Open -> Try to find the Song for the given file and then play it from all songs
is DeferredPlayback.Open -> { is DeferredPlayback.Open -> {
L.d("Opening specified file") L.d("Opening specified file")
deviceLibrary.findSongForUri(context, action.uri)?.let { song -> // library.findSongForUri(context, action.uri)?.let { song ->
playbackManager.play( // playbackManager.play(
requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { // requireNotNull(commandFactory.song(song,
"Invalid playback parameters" // ShuffleMode.IMPLICIT)) {
}) // "Invalid playback parameters"
} // })
// }
} }
} }
@ -498,7 +499,7 @@ class ExoPlaybackStateHolder(
// --- MUSICREPOSITORY METHODS --- // --- MUSICREPOSITORY METHODS ---
override fun onMusicChanges(changes: MusicRepository.Changes) { 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. // We now have a library, see if we have anything we need to do.
L.d("Library obtained, requesting action") L.d("Library obtained, requesting action")
playbackManager.requestAction(this) 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song 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.info.Name
import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MediaSessionUID
import org.oxycblt.auxio.music.service.MusicBrowser 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.PlaybackCommand
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
@ -92,23 +91,21 @@ constructor(
override fun onPlayFromSearch(query: String, extras: Bundle) { override fun onPlayFromSearch(query: String, extras: Bundle) {
super.onPlayFromSearch(query, extras) super.onPlayFromSearch(query, extras)
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
val userLibrary = musicRepository.userLibrary ?: return val command = expandSearchInfoCommand(query.ifBlank { null }, extras, library)
val command =
expandSearchInfoCommand(query.ifBlank { null }, extras, deviceLibrary, userLibrary)
playbackManager.play(requireNotNull(command) { "Invalid playback configuration" }) playbackManager.play(requireNotNull(command) { "Invalid playback configuration" })
} }
override fun onAddQueueItem(description: MediaDescriptionCompat) { override fun onAddQueueItem(description: MediaDescriptionCompat) {
super.onAddQueueItem(description) super.onAddQueueItem(description)
val deviceLibrary = musicRepository.deviceLibrary ?: return val library = musicRepository.library ?: return
val uid = MediaSessionUID.fromString(description.mediaId ?: return) ?: return val uid = MediaSessionUID.fromString(description.mediaId ?: return) ?: return
val songUid = val songUid =
when (uid) { when (uid) {
is MediaSessionUID.SingleItem -> uid.uid is MediaSessionUID.SingleItem -> uid.uid
else -> return else -> return
} }
val song = deviceLibrary.songs.find { it.uid == songUid } ?: return val song = library.songs.find { it.uid == songUid } ?: return
playbackManager.addToQueue(song) playbackManager.addToQueue(song)
} }
@ -208,8 +205,7 @@ constructor(
private fun expandSearchInfoCommand( private fun expandSearchInfoCommand(
query: String?, query: String?,
extras: Bundle, extras: Bundle,
deviceLibrary: DeviceLibrary, library: Library
userLibrary: UserLibrary
): PlaybackCommand? { ): PlaybackCommand? {
if (query == null) { if (query == null) {
// User just wanted to 'play some music', shuffle all // User just wanted to 'play some music', shuffle all
@ -222,7 +218,7 @@ constructor(
val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)
val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)
val best = val best =
deviceLibrary.songs.maxByOrNull { library.songs.maxByOrNull {
fuzzy(it.name, songQuery) + fuzzy(it.name, songQuery) +
fuzzy(it.album.name, albumQuery) + fuzzy(it.album.name, albumQuery) +
it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) } it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) }
@ -235,7 +231,7 @@ constructor(
val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)
val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)
val best = val best =
deviceLibrary.albums.maxByOrNull { library.albums.maxByOrNull {
fuzzy(it.name, albumQuery) + fuzzy(it.name, albumQuery) +
it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) } it.artists.maxOf { artist -> fuzzy(artist.name, artistQuery) }
} }
@ -245,21 +241,21 @@ constructor(
} }
MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> { MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> {
val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) 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) { if (best != null) {
return commandFactory.artist(best, ShuffleMode.OFF) return commandFactory.artist(best, ShuffleMode.OFF)
} }
} }
MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> { MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> {
val genreQuery = extras.getString(MediaStore.EXTRA_MEDIA_GENRE) 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) { if (best != null) {
return commandFactory.genre(best, ShuffleMode.OFF) return commandFactory.genre(best, ShuffleMode.OFF)
} }
} }
MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE -> { MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE -> {
val playlistQuery = extras.getString(MediaStore.EXTRA_MEDIA_PLAYLIST) 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) { if (best != null) {
return commandFactory.playlist(best, ShuffleMode.OFF) return commandFactory.playlist(best, ShuffleMode.OFF)
} }
@ -268,11 +264,7 @@ constructor(
} }
val bestMusic = val bestMusic =
(deviceLibrary.songs + (library.songs + library.albums + library.artists + library.genres + library.playlists)
deviceLibrary.albums +
deviceLibrary.artists +
deviceLibrary.genres +
userLibrary.playlists)
.maxByOrNull { fuzzy(it.name, query) } .maxByOrNull { fuzzy(it.name, query) }
// TODO: Error out when we can't correctly resolve the query // TODO: Error out when we can't correctly resolve the query
return bestMusic?.let { expandMusicIntoCommand(it, null) } return bestMusic?.let { expandMusicIntoCommand(it, null) }

View file

@ -147,8 +147,8 @@ constructor(
} }
private fun newCommand(song: Song?, shuffle: ShuffleMode): PlaybackCommand? { private fun newCommand(song: Song?, shuffle: ShuffleMode): PlaybackCommand? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null val library = musicRepository.library ?: return null
return newCommand(song, null, deviceLibrary.songs, listSettings.songSort, shuffle) return newCommand(song, null, library.songs, listSettings.songSort, shuffle)
} }
private fun newCommand( 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.Item
import org.oxycblt.auxio.list.PlainDivider import org.oxycblt.auxio.list.PlainDivider
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song 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.PlaySong
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import timber.log.Timber as L import timber.log.Timber as L
@ -95,9 +94,8 @@ constructor(
currentSearchJob?.cancel() currentSearchJob?.cancel()
lastQuery = query lastQuery = query
val deviceLibrary = musicRepository.deviceLibrary val library = musicRepository.library
val userLibrary = musicRepository.userLibrary if (query.isNullOrEmpty() || library == null) {
if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) {
L.d("Cannot search for the current query, aborting") L.d("Cannot search for the current query, aborting")
_searchResults.value = listOf() _searchResults.value = listOf()
return return
@ -107,16 +105,11 @@ constructor(
L.d("Searching music library for $query") L.d("Searching music library for $query")
currentSearchJob = currentSearchJob =
viewModelScope.launch { viewModelScope.launch {
_searchResults.value = _searchResults.value = searchImpl(library, query).also { yield() }
searchImpl(deviceLibrary, userLibrary, query).also { yield() }
} }
} }
private suspend fun searchImpl( private suspend fun searchImpl(library: Library, query: String): List<Item> {
deviceLibrary: DeviceLibrary,
userLibrary: UserLibrary,
query: String
): List<Item> {
val filter = searchSettings.filterTo val filter = searchSettings.filterTo
val items = val items =
@ -124,19 +117,19 @@ constructor(
// A nulled filter type means to not filter anything. // A nulled filter type means to not filter anything.
L.d("No filter specified, using entire library") L.d("No filter specified, using entire library")
SearchEngine.Items( SearchEngine.Items(
deviceLibrary.songs, library.songs,
deviceLibrary.albums, library.albums,
deviceLibrary.artists, library.artists,
deviceLibrary.genres, library.genres,
userLibrary.playlists) library.playlists)
} else { } else {
L.d("Filter specified, reducing library") L.d("Filter specified, reducing library")
SearchEngine.Items( SearchEngine.Items(
songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null, songs = if (filter == MusicType.SONGS) library.songs else null,
albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null, albums = if (filter == MusicType.ALBUMS) library.albums else null,
artists = if (filter == MusicType.ARTISTS) deviceLibrary.artists else null, artists = if (filter == MusicType.ARTISTS) library.artists else null,
genres = if (filter == MusicType.GENRES) deviceLibrary.genres else null, genres = if (filter == MusicType.GENRES) library.genres else null,
playlists = if (filter == MusicType.PLAYLISTS) userLibrary.playlists else null) playlists = if (filter == MusicType.PLAYLISTS) library.playlists else null)
} }
val results = searchEngine.search(items, query) 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.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace 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.parseId3v2PositionField
import org.oxycblt.auxio.music.stack.explore.extractor.parseVorbisPositionField import org.oxycblt.auxio.music.stack.explore.extractor.parseVorbisPositionField
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
import org.oxycblt.auxio.music.stack.interpret.prepare.parseId3GenreNames
class TagUtilTest { class TagUtilTest {
@Test @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())
}
}