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