music: connect new loader to rest of app
This commit is contained in:
parent
e3d6644634
commit
ba29905aa6
52 changed files with 915 additions and 1116 deletions
|
@ -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))
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 } }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +71,13 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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"]
|
||||||
|
?: vorbis["musicbrainz release track id"]
|
||||||
?: id3v2["TXXX:musicbrainz release track id"]
|
?: id3v2["TXXX:musicbrainz release track id"]
|
||||||
?: id3v2["TXXX:musicbrainz_releasetrackid"])?.first()
|
?: 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() =
|
||||||
|
(parseVorbisPositionField(
|
||||||
vorbis["tracknumber"]?.first(),
|
vorbis["tracknumber"]?.first(),
|
||||||
(vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.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() =
|
||||||
|
(parseVorbisPositionField(
|
||||||
vorbis["discnumber"]?.first(),
|
vorbis["discnumber"]?.first(),
|
||||||
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
|
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
|
||||||
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
|
?: 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,9 +62,11 @@ 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["originaldate"]?.run { Date.from(first()) }
|
||||||
?: vorbis["date"]?.run { Date.from(first()) }
|
?: vorbis["date"]?.run { Date.from(first()) }
|
||||||
?: vorbis["year"]?.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:
|
||||||
|
@ -58,48 +81,60 @@ fun TextTags.date() = (vorbis["originaldate"]?.run { Date.from(first()) }
|
||||||
|
|
||||||
// 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["artistssort"]
|
||||||
?: vorbis["artists_sort"]
|
?: vorbis["artists_sort"]
|
||||||
?: vorbis["artists sort"]
|
?: vorbis["artists sort"]
|
||||||
?: vorbis["artistsort"]
|
?: vorbis["artistsort"]
|
||||||
?: vorbis["artist sort"] ?: id3v2["TXXX:artistssort"]
|
?: vorbis["artist sort"]
|
||||||
|
?: id3v2["TXXX:artistssort"]
|
||||||
?: id3v2["TXXX:artists_sort"]
|
?: id3v2["TXXX:artists_sort"]
|
||||||
?: id3v2["TXXX:artists sort"]
|
?: id3v2["TXXX:artists sort"]
|
||||||
?: id3v2["TSOP"]
|
?: id3v2["TSOP"]
|
||||||
?: id3v2["artistsort"]
|
?: id3v2["artistsort"]
|
||||||
?: id3v2["TXXX:artist sort"]
|
?: 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"]
|
||||||
|
@ -109,41 +144,45 @@ fun TextTags.albumArtistNames() = (
|
||||||
?: 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["albumartistssort"]
|
||||||
?: vorbis["albumartists_sort"]
|
?: vorbis["albumartists_sort"]
|
||||||
?: vorbis["albumartists sort"]
|
?: vorbis["albumartists sort"]
|
||||||
?: vorbis["albumartistsort"]
|
?: vorbis["albumartistsort"]
|
||||||
?: vorbis["album artist sort"] ?: id3v2["TXXX:albumartistssort"]
|
?: vorbis["album artist sort"]
|
||||||
|
?: id3v2["TXXX:albumartistssort"]
|
||||||
?: id3v2["TXXX:albumartists_sort"]
|
?: id3v2["TXXX:albumartists_sort"]
|
||||||
?: id3v2["TXXX:albumartists sort"]
|
?: id3v2["TXXX:albumartists sort"]
|
||||||
?: id3v2["TXXX:albumartistsort"]
|
?: id3v2["TXXX:albumartistsort"]
|
||||||
// This is a non-standard iTunes extension
|
// This is a non-standard iTunes extension
|
||||||
?: id3v2["TSO2"]
|
?: id3v2["TSO2"]
|
||||||
?: id3v2["TXXX:album artist sort"]
|
?: 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() =
|
||||||
|
(vorbis["compilation"]
|
||||||
|
?: vorbis["itunescompilation"]
|
||||||
?: id3v2["TCMP"] // This is a non-standard itunes extension
|
?: id3v2["TCMP"] // This is a non-standard itunes extension
|
||||||
?: id3v2["TXXX:compilation"] ?: id3v2["TXXX:itunescompilation"]
|
?: id3v2["TXXX:compilation"]
|
||||||
)
|
?: id3v2["TXXX:itunescompilation"])
|
||||||
?.let {
|
?.let {
|
||||||
// Ignore invalid instances of this tag
|
// Ignore invalid instances of this tag
|
||||||
it == listOf("1")
|
it == listOf("1")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayGain information
|
// ReplayGain information
|
||||||
fun TextTags.replayGainTrackAdjustment() = (vorbis["r128_track_gain"]?.parseR128Adjustment()
|
fun TextTags.replayGainTrackAdjustment() =
|
||||||
|
(vorbis["r128_track_gain"]?.parseR128Adjustment()
|
||||||
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
|
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
|
||||||
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
|
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||||
|
|
||||||
fun TextTags.replayGainAlbumAdjustment() = (vorbis["r128_album_gain"]?.parseR128Adjustment()
|
fun TextTags.replayGainAlbumAdjustment() =
|
||||||
|
(vorbis["r128_album_gain"]?.parseR128Adjustment()
|
||||||
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
|
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
|
||||||
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment())
|
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment())
|
||||||
|
|
||||||
|
@ -182,11 +221,7 @@ 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, "")
|
|
||||||
.toFloatOrNull()
|
|
||||||
?.nonZeroOrNull()
|
|
||||||
?.run {
|
|
||||||
// Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale
|
// Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale
|
||||||
this / 256f + 5
|
this / 256f + 5
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -107,4 +107,3 @@ constructor(
|
||||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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>) =
|
||||||
|
linkedSongs.map {
|
||||||
val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase()
|
val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase()
|
||||||
val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId
|
val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId
|
||||||
val albumLink = tree.getOrPut(nameKey) { mutableMapOf() }
|
val albumLink =
|
||||||
|
tree
|
||||||
|
.getOrPut(nameKey) { mutableMapOf() }
|
||||||
.getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) }
|
.getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) }
|
||||||
albumLink.node.contributors.contribute(it.linkedAlbum)
|
albumLink.node.contributors.contribute(it.linkedAlbum)
|
||||||
LinkedSong(it, albumLink)
|
LinkedSong(it, albumLink)
|
||||||
|
@ -30,15 +41,17 @@ class AlbumLinker {
|
||||||
|
|
||||||
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 =
|
||||||
|
musicBrainzIdBundle[null]
|
||||||
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
|
?: 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
|
||||||
|
.filter { it.key != null }
|
||||||
|
.forEach {
|
||||||
val candidates = it.value.node.contributors.candidates
|
val candidates = it.value.node.contributors.candidates
|
||||||
nullBundle.node.contributors.contribute(candidates)
|
nullBundle.node.contributors.contribute(candidates)
|
||||||
it.value.node = nullBundle.node
|
it.value.node = nullBundle.node
|
||||||
|
@ -47,23 +60,18 @@ class AlbumLinker {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -1,58 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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 linkedSongArtists =
|
||||||
|
it.preSong.preArtists.map { artist ->
|
||||||
val nameKey = artist.rawName?.lowercase()
|
val nameKey = artist.rawName?.lowercase()
|
||||||
val musicBrainzIdKey = artist.musicBrainzId
|
val musicBrainzIdKey = artist.musicBrainzId
|
||||||
val artistLink = tree.getOrPut(nameKey) { mutableMapOf() }
|
val artistLink =
|
||||||
|
tree
|
||||||
|
.getOrPut(nameKey) { mutableMapOf() }
|
||||||
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
||||||
artistLink.node.contributors.contribute(artist)
|
artistLink.node.contributors.contribute(artist)
|
||||||
artistLink
|
artistLink
|
||||||
}
|
}
|
||||||
val linkedAlbumArtists = it.preSong.preAlbum.preArtists.map { artist ->
|
val linkedAlbumArtists =
|
||||||
|
it.preSong.preAlbum.preArtists.map { artist ->
|
||||||
val nameKey = artist.rawName?.lowercase()
|
val nameKey = artist.rawName?.lowercase()
|
||||||
val musicBrainzIdKey = artist.musicBrainzId
|
val musicBrainzIdKey = artist.musicBrainzId
|
||||||
val artistLink = tree.getOrPut(nameKey) { mutableMapOf() }
|
val artistLink =
|
||||||
|
tree
|
||||||
|
.getOrPut(nameKey) { mutableMapOf() }
|
||||||
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
||||||
artistLink.node.contributors.contribute(artist)
|
artistLink.node.contributors.contribute(artist)
|
||||||
artistLink
|
artistLink
|
||||||
}
|
}
|
||||||
val linkedAlbum =
|
val linkedAlbum = LinkedAlbum(it.preSong.preAlbum, MultiArtistLink(linkedAlbumArtists))
|
||||||
LinkedAlbum(it.preSong.preAlbum, MultiArtistLink(linkedAlbumArtists))
|
|
||||||
LinkedSong(it, linkedAlbum, MultiArtistLink(linkedSongArtists))
|
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 =
|
||||||
|
musicBrainzIdBundle[null]
|
||||||
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
|
?: 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
|
||||||
|
.filter { it.key != null }
|
||||||
|
.forEach {
|
||||||
val candidates = it.value.node.contributors.candidates
|
val candidates = it.value.node.contributors.candidates
|
||||||
nullBundle.node.contributors.contribute(candidates)
|
nullBundle.node.contributors.contribute(candidates)
|
||||||
it.value.node = nullBundle.node
|
it.value.node = nullBundle.node
|
||||||
|
@ -71,19 +90,17 @@ 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" }
|
||||||
|
.also {
|
||||||
when (child) {
|
when (child) {
|
||||||
is SongImpl -> it.link(child)
|
is SongImpl -> it.link(child)
|
||||||
is AlbumImpl -> it.link(child)
|
is AlbumImpl -> it.link(child)
|
||||||
|
@ -93,9 +110,7 @@ class ArtistLinker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
}
|
}
|
|
@ -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,8 +28,10 @@ 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 genreLinks =
|
||||||
|
it.preGenres.map { genre ->
|
||||||
val nameKey = genre.rawName?.lowercase()
|
val nameKey = genre.rawName?.lowercase()
|
||||||
val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) }
|
val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) }
|
||||||
link.node.contributors.contribute(genre)
|
link.node.contributors.contribute(genre)
|
||||||
|
@ -22,35 +40,25 @@ class GenreLinker {
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
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,10 +130,10 @@ 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 =
|
||||||
|
dates?.let {
|
||||||
if (song.date < it.min) Date.Range(song.date, it.max)
|
if (song.date < it.min) Date.Range(song.date, it.max)
|
||||||
else if (song.date > it.max) Date.Range(it.min, song.date)
|
else if (song.date > it.max) Date.Range(it.min, song.date) else it
|
||||||
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
|
||||||
|
@ -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)"
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
package org.oxycblt.auxio.music.stack.interpret.prepare
|
/*
|
||||||
|
* Copyright (c) 2024 Auxio Project
|
||||||
|
* PrepareModule.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.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)
|
||||||
|
|
|
@ -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,30 +36,35 @@ 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 = makePreArtists(
|
val albumPreArtists =
|
||||||
|
makePreArtists(
|
||||||
audioFile.albumArtistMusicBrainzIds,
|
audioFile.albumArtistMusicBrainzIds,
|
||||||
audioFile.albumArtistNames,
|
audioFile.albumArtistNames,
|
||||||
audioFile.albumArtistSortNames
|
audioFile.albumArtistSortNames,
|
||||||
)
|
interpretation)
|
||||||
val preAlbum = makePreAlbum(audioFile, individualPreArtists, albumPreArtists)
|
val preAlbum =
|
||||||
|
makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation)
|
||||||
val rawArtists =
|
val rawArtists =
|
||||||
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
individualPreArtists
|
||||||
|
.ifEmpty { albumPreArtists }
|
||||||
|
.ifEmpty { listOf(unknownPreArtist()) }
|
||||||
val rawGenres =
|
val rawGenres =
|
||||||
makePreGenres(audioFile).ifEmpty { listOf(unknownPreGenre()) }
|
makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
||||||
val uri = audioFile.deviceFile.uri
|
val uri = audioFile.deviceFile.uri
|
||||||
PreSong(
|
PreSong(
|
||||||
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
|
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
|
||||||
name = nameFactory.parse(need(audioFile, "name", audioFile.name), audioFile.sortName),
|
name =
|
||||||
|
interpretation.nameFactory.parse(
|
||||||
|
need(audioFile, "name", audioFile.name), audioFile.sortName),
|
||||||
rawName = audioFile.name,
|
rawName = audioFile.name,
|
||||||
track = audioFile.track,
|
track = audioFile.track,
|
||||||
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
|
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
|
||||||
|
@ -49,13 +72,12 @@ class PreparerImpl(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
cover = inferCover(audioFile),
|
cover = inferCover(audioFile),
|
||||||
path = need(audioFile, "path", audioFile.deviceFile.path),
|
path = need(audioFile, "path", audioFile.deviceFile.path),
|
||||||
mimeType = MimeType(
|
mimeType =
|
||||||
need(audioFile, "mime type", audioFile.deviceFile.mimeType),
|
MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null),
|
||||||
null
|
|
||||||
),
|
|
||||||
size = audioFile.deviceFile.size,
|
size = audioFile.deviceFile.size,
|
||||||
durationMs = need(audioFile, "duration", audioFile.durationMs),
|
durationMs = need(audioFile, "duration", audioFile.durationMs),
|
||||||
replayGainAdjustment = ReplayGainAdjustment(
|
replayGainAdjustment =
|
||||||
|
ReplayGainAdjustment(
|
||||||
audioFile.replayGainTrackAdjustment,
|
audioFile.replayGainTrackAdjustment,
|
||||||
audioFile.replayGainAlbumAdjustment,
|
audioFile.replayGainAlbumAdjustment,
|
||||||
),
|
),
|
||||||
|
@ -63,32 +85,29 @@ class PreparerImpl(
|
||||||
dateAdded = audioFile.deviceFile.lastModified,
|
dateAdded = audioFile.deviceFile.lastModified,
|
||||||
preAlbum = preAlbum,
|
preAlbum = preAlbum,
|
||||||
preArtists = rawArtists,
|
preArtists = rawArtists,
|
||||||
preGenres = rawGenres
|
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.parse(interpretation.separators.split(audioFile.releaseTypes))
|
||||||
?: ReleaseType.Album(null),
|
?: ReleaseType.Album(null),
|
||||||
preArtists =
|
preArtists =
|
||||||
albumPreArtists
|
albumPreArtists
|
||||||
|
@ -99,47 +118,47 @@ class PreparerImpl(
|
||||||
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)
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue