diff --git a/CHANGELOG.md b/CHANGELOG.md index 238d43aab..3cb8cd40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,6 @@ deletion #### What's Changed - "Ignore articles when sorting" is now "Intelligent sorting" -#### What's Changed -- "Ignore articles when sorting" is now "Intelligent sorting" - ## 3.0.3 #### What's New diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 8369fcf67..8b2baac6c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -53,7 +53,7 @@ class DetailViewModel @Inject constructor( private val musicRepository: MusicRepository, - private val audioInfoProvider: AudioInfo.Provider, + private val audioInfoFactory: AudioInfo.Factory, private val musicSettings: MusicSettings, private val playbackSettings: PlaybackSettings ) : ViewModel(), MusicRepository.UpdateListener { @@ -308,7 +308,7 @@ constructor( _songAudioInfo.value = null currentSongJob = viewModelScope.launch(Dispatchers.IO) { - val info = audioInfoProvider.extract(song) + val info = audioInfoFactory.extract(song) yield() _songAudioInfo.value = info } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 6295dda5b..bcd001aa7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -172,8 +172,8 @@ constructor( private val cacheRepository: CacheRepository, private val mediaStoreExtractor: MediaStoreExtractor, private val tagExtractor: TagExtractor, - private val deviceLibraryProvider: DeviceLibrary.Provider, - private val userLibraryProvider: UserLibrary.Provider + private val deviceLibraryFactory: DeviceLibrary.Factory, + private val userLibraryFactory: UserLibrary.Factory ) : MusicRepository { private val updateListeners = mutableListOf() private val indexingListeners = mutableListOf() @@ -314,9 +314,9 @@ constructor( val deviceLibraryChannel = Channel() val deviceLibraryJob = worker.scope.async(Dispatchers.Main) { - deviceLibraryProvider.create(rawSongs).also { deviceLibraryChannel.send(it) } + deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } } - val userLibraryJob = worker.scope.async { userLibraryProvider.read(deviceLibraryChannel) } + val userLibraryJob = worker.scope.async { userLibraryFactory.read(deviceLibraryChannel) } if (cache == null || cache.invalidated) { cacheRepository.writeCache(rawSongs) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 404aa8f0c..f292303b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -89,7 +89,7 @@ interface DeviceLibrary { fun findGenre(uid: Music.UID): Genre? /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ - interface Provider { + interface Factory { /** * Create a new [DeviceLibrary]. * @@ -110,8 +110,8 @@ interface DeviceLibrary { } } -class DeviceLibraryProviderImpl @Inject constructor(private val musicSettings: MusicSettings) : - DeviceLibrary.Provider { +class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) : + DeviceLibrary.Factory { override suspend fun create(rawSongs: List): DeviceLibrary = DeviceLibraryImpl(rawSongs, musicSettings) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt index 3bb3de657..41b69a498 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt @@ -26,6 +26,5 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface DeviceModule { - @Binds - fun deviceLibraryProvider(providerImpl: DeviceLibraryProviderImpl): DeviceLibrary.Provider + @Binds fun deviceLibraryProvider(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index a274209e9..76f78c4e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -342,8 +342,8 @@ class ArtistImpl( override val durationMs: Long? override val isCollaborator: Boolean - // Note: Append song contents to MusicParent equality so that Groups with - // the same UID but different contents are not equal. + // Note: Append song contents to MusicParent equality so that artists with + // the same UID but different songs are not equal. override fun hashCode() = 31 * uid.hashCode() + songs.hashCode() override fun equals(other: Any?) = other is ArtistImpl && uid == other.uid && songs == other.songs diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 86b8df817..571e90ff5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -186,13 +186,13 @@ class RawGenre( /** @see Music.rawName */ val name: String? = null ) { - // Only group by the lowercase genre name. This allows Genre grouping to be - // case-insensitive, which may be helpful in some libraries with different ways of - // formatting genres. // Cache the hashCode for HashMap efficiency. private val hashCode = name?.lowercase().hashCode() + // Only group by the lowercase genre name. This allows Genre grouping to be + // case-insensitive, which may be helpful in some libraries with different ways of + // formatting genres. override fun hashCode() = hashCode override fun equals(other: Any?) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index f97318d5c..9e3d1f2d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -178,8 +178,8 @@ private abstract class BaseMediaStoreExtractor( while (cursor.moveToNext()) { // Assume that a song can't inhabit multiple genre entries, as I - // doubt - // MediaStore is actually aware that songs can have multiple genres. + // doubt MediaStore is actually aware that songs can have multiple + // genres. genreNamesMap[cursor.getLong(songIdIndex)] = name } } @@ -311,9 +311,8 @@ private abstract class BaseMediaStoreExtractor( rawSong.date = cursor.getStringOrNull(yearIndex)?.let(Date::from) // A non-existent album name should theoretically be the name of the folder it contained // in, but in practice it is more often "0" (as in /storage/emulated/0), even when it - // the - // file is not actually in the root internal storage directory. We can't do anything to - // fix this, really. + // the file is not actually in the root internal storage directory. We can't do + // anything to fix this, really. rawSong.albumName = cursor.getString(albumIndex) // Android does not make a non-existent artist tag null, it instead fills it in // as , which makes absolutely no sense given how other columns default @@ -356,9 +355,6 @@ private abstract class BaseMediaStoreExtractor( // Note: The separation between version-specific backends may not be the cleanest. To preserve // speed, we only want to add redundancy on known issues, not with possible issues. -// Note: The separation between version-specific backends may not be the cleanest. To preserve -// speed, we only want to add redundancy on known issues, not with possible issues. - private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) : BaseMediaStoreExtractor(context, musicSettings) { override val projection: Array diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt index 5f801a1e5..2dc906563 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt @@ -43,7 +43,7 @@ data class AudioInfo( val resolvedMimeType: MimeType ) { /** Implements the process of extracting [AudioInfo] from a given [Song]. */ - interface Provider { + interface Factory { /** * Extract the [AudioInfo] of a given [Song]. * @@ -55,12 +55,12 @@ data class AudioInfo( } /** - * A framework-backed implementation of [AudioInfo.Provider]. + * A framework-backed implementation of [AudioInfo.Factory]. * * @param context [Context] required to read audio files. */ -class AudioInfoProviderImpl @Inject constructor(@ApplicationContext private val context: Context) : - AudioInfo.Provider { +class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val context: Context) : + AudioInfo.Factory { override suspend fun extract(song: Song): AudioInfo { // While we would use ExoPlayer to extract this information, it doesn't support diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt index d6be65f67..650acbe60 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt @@ -26,7 +26,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface MetadataModule { - @Binds fun tagExtractor(tagExtractor: TagExtractorImpl): TagExtractor - @Binds fun tagWorkerFactory(taskFactory: TagWorkerImpl.Factory): TagWorker.Factory - @Binds fun audioInfoProvider(audioInfoProvider: AudioInfoProviderImpl): AudioInfo.Provider + @Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor + @Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory + @Binds fun audioInfoProvider(factory: AudioInfoFactoryImpl): AudioInfo.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 04ca06409..1a553211c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -56,14 +56,26 @@ interface TagWorker { } } -class TagWorkerImpl -private constructor(private val rawSong: RawSong, private val future: Future) : - TagWorker { - /** - * Try to get a completed song from this [TagWorker], if it has finished processing. - * - * @return A [RawSong] instance if processing has completed, null otherwise. - */ +class TagWorkerFactoryImpl +@Inject +constructor(private val mediaSourceFactory: MediaSource.Factory) : TagWorker.Factory { + override fun create(rawSong: RawSong): TagWorker = + // Note that we do not leverage future callbacks. This is because errors in the + // (highly fallible) extraction process will not bubble up to Indexer when a + // listener is used, instead crashing the app entirely. + TagWorkerImpl( + rawSong, + MetadataRetriever.retrieveMetadata( + mediaSourceFactory, + MediaItem.fromUri( + requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))) +} + +private class TagWorkerImpl( + private val rawSong: RawSong, + private val future: Future +) : TagWorker { + override fun poll(): RawSong? { if (!future.isDone) { // Not done yet, nothing to do. @@ -95,12 +107,6 @@ private constructor(private val rawSong: RawSong, private val future: Future>) { // Song textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() } @@ -169,16 +175,6 @@ private constructor(private val rawSong: RawSong, private val future: Future>): Date? { // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY // is present. @@ -212,11 +208,6 @@ private constructor(private val rawSong: RawSong, private val future: Future>) { // Song comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() } @@ -277,21 +268,6 @@ private constructor(private val rawSong: RawSong, private val future: Future, - musicSettings: MusicSettings + override val sortName: SortName, + override val songs: List ) : Playlist { - constructor( - name: String, - songs: List, - musicSettings: MusicSettings - ) : this(Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), name, songs, musicSettings) - - constructor( - rawPlaylist: RawPlaylist, - deviceLibrary: DeviceLibrary, - musicSettings: MusicSettings - ) : this( - rawPlaylist.playlistInfo.playlistUid, - rawPlaylist.playlistInfo.name, - rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }, - musicSettings) - override fun resolveName(context: Context) = rawName override val rawSortName = null - override val sortName = SortName(rawName, musicSettings) override val durationMs = songs.sumOf { it.durationMs } override val albums = songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } + + /** + * Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s. + * + * @param songs The new [Song]s to use. + */ + fun edit(songs: List) = PlaylistImpl(uid, rawName, sortName, songs) + + /** + * Clone the data in this instance to a new [PlaylistImpl] with the given [edits]. + * + * @param edits The edits to make to the [Song]s of the playlist. + */ + inline fun edit(edits: MutableList.() -> Unit) = edit(songs.toMutableList().apply(edits)) + + companion object { + /** + * Create a new instance with a novel UID. + * + * @param name The name of the playlist. + * @param songs The songs to initially populate the playlist with. + * @param musicSettings [MusicSettings] required for name configuration. + */ + fun new(name: String, songs: List, musicSettings: MusicSettings) = + PlaylistImpl( + Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), + name, + SortName(name, musicSettings), + songs) + + /** + * Populate a new instance from a read [RawPlaylist]. + * + * @param rawPlaylist The [RawPlaylist] to read from. + * @param deviceLibrary The [DeviceLibrary] to initialize from. + * @param musicSettings [MusicSettings] required for name configuration. + */ + fun fromRaw( + rawPlaylist: RawPlaylist, + deviceLibrary: DeviceLibrary, + musicSettings: MusicSettings + ) = + PlaylistImpl( + rawPlaylist.playlistInfo.playlistUid, + rawPlaylist.playlistInfo.name, + SortName(rawPlaylist.playlistInfo.name, musicSettings), + rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 01fada793..f34ae6648 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -45,23 +45,46 @@ interface UserLibrary { fun findPlaylist(uid: Music.UID): Playlist? /** Constructs a [UserLibrary] implementation in an asynchronous manner. */ - interface Provider { + interface Factory { /** * Create a new [UserLibrary]. * * @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later. * This allows database information to be read before the actual instance is constructed. - * @return A new [UserLibrary] with the required implementation. + * @return A new [MutableUserLibrary] with the required implementation. */ - suspend fun read(deviceLibrary: Channel): UserLibrary + suspend fun read(deviceLibrary: Channel): MutableUserLibrary } } -class UserLibraryProviderImpl +/** + * A mutable instance of [UserLibrary]. Not meant for use outside of the music module. Use + * [MusicRepository] instead. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface MutableUserLibrary : UserLibrary { + /** + * Make a new [Playlist]. + * + * @param name The name of the [Playlist]. + * @param songs The songs to place in the [Playlist]. + */ + fun createPlaylist(name: String, songs: List) + + /** + * Add [Song]s to a [Playlist]. + * + * @param playlist The [Playlist] to add to. Must currently exist. + */ + fun addToPlaylist(playlist: Playlist, songs: List) +} + +class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) : - UserLibrary.Provider { - override suspend fun read(deviceLibrary: Channel): UserLibrary = + UserLibrary.Factory { + override suspend fun read(deviceLibrary: Channel): MutableUserLibrary = UserLibraryImpl(playlistDao, deviceLibrary.receive(), musicSettings) } @@ -69,15 +92,28 @@ private class UserLibraryImpl( private val playlistDao: PlaylistDao, private val deviceLibrary: DeviceLibrary, private val musicSettings: MusicSettings -) : UserLibrary { +) : MutableUserLibrary { private val playlistMap = mutableMapOf() override val playlists: List get() = playlistMap.values.toList() init { - val playlist = PlaylistImpl("Playlist 1", deviceLibrary.songs.slice(58..100), musicSettings) - playlistMap[playlist.uid] = playlist + // TODO: Actually read playlists + createPlaylist("Playlist 1", deviceLibrary.songs.slice(58..100)) } override fun findPlaylist(uid: Music.UID) = playlistMap[uid] + + @Synchronized + override fun createPlaylist(name: String, songs: List) { + val playlistImpl = PlaylistImpl.new(name, songs, musicSettings) + playlistMap[playlistImpl.uid] = playlistImpl + } + + @Synchronized + override fun addToPlaylist(playlist: Playlist, songs: List) { + val playlistImpl = + requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } + playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt index 9c92c0ca5..b4c7ef6a4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt @@ -30,7 +30,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface UserModule { - @Binds fun userLibaryProvider(provider: UserLibraryProviderImpl): UserLibrary.Provider + @Binds fun userLibaryFactory(factory: UserLibraryFactoryImpl): UserLibrary.Factory } @Module