diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 285e94c2c..fe25c84fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -30,12 +30,12 @@ import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.music.stack.fs.MimeType -import org.oxycblt.auxio.music.stack.fs.Path 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.fs.MimeType +import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull 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 34d21ed43..2ef61e650 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -21,27 +21,20 @@ package org.oxycblt.auxio.music import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -import java.util.LinkedList import javax.inject.Inject import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary -import org.oxycblt.auxio.music.device.RawSong 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.util.DEFAULT_TIMEOUT -import org.oxycblt.auxio.util.forEachWithTimeout import timber.log.Timber as L /** @@ -220,9 +213,7 @@ interface MusicRepository { class MusicRepositoryImpl @Inject constructor( - private val cacheRepository: CacheRepository, - private val mediaStoreExtractor: MediaStoreExtractor, - private val tagExtractor: TagExtractor, + private val indexer: Indexer, private val deviceLibraryFactory: DeviceLibrary.Factory, private val userLibraryFactory: UserLibrary.Factory, private val musicSettings: MusicSettings @@ -355,9 +346,6 @@ constructor( } private suspend fun indexImpl(context: Context, scope: CoroutineScope, withCache: Boolean) { - // TODO: Find a way to break up this monster of a method, preferably as another class. - - val start = System.currentTimeMillis() // Make sure we have permissions before going forward. Theoretically this would be better // done at the UI level, but that intertwines logic and display too much. if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == @@ -367,8 +355,6 @@ constructor( } // Obtain configuration information - val constraints = - MediaStoreExtractor.Constraints(musicSettings.excludeNonMusic, musicSettings.musicDirs) val separators = Separators.from(musicSettings.separators) val nameFactory = if (musicSettings.intelligentSorting) { @@ -377,174 +363,7 @@ constructor( Name.Known.SimpleFactory } - // Begin with querying MediaStore and the music cache. The former is needed for Auxio - // to figure out what songs are (probably) on the device, and the latter will be needed - // for discovery (described later). These have no shared state, so they are done in - // parallel. - L.d("Starting MediaStore query") - emitIndexingProgress(IndexingProgress.Indeterminate) - - val mediaStoreQueryJob = - scope.async { - val query = - try { - mediaStoreExtractor.query(constraints) - } catch (e: Exception) { - // Normally, errors in an async call immediately bubble up to the Looper - // and crash the app. Thus, we have to wrap any error into a Result - // and then manually forward it to the try block that indexImpl is - // called from. - return@async Result.failure(e) - } - Result.success(query) - } - // Since this main thread is a co-routine, we can do operations in parallel in a way - // identical to calling async. - val cache = - if (withCache) { - L.d("Reading cache") - cacheRepository.readCache() - } else { - null - } - L.d("Awaiting MediaStore query") - val query = mediaStoreQueryJob.await().getOrThrow() - - // We now have all the information required to start the "discovery" process. This - // is the point at which Auxio starts scanning each file given from MediaStore and - // transforming it into a music library. MediaStore normally - L.d("Starting discovery") - val incompleteSongs = Channel(Channel.UNLIMITED) // Not fully populated w/metadata - val completeSongs = Channel(Channel.UNLIMITED) // Populated with quality metadata - val processedSongs = Channel(Channel.UNLIMITED) // Transformed into SongImpl - - // MediaStoreExtractor discovers all music on the device, and forwards them to either - // DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata - // does not exist. In the latter situation, it also applies it's own (inferior) metadata. - L.d("Starting MediaStore discovery") - val mediaStoreJob = - scope.async { - try { - mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) - } catch (e: Exception) { - // To prevent a deadlock, we want to close the channel with an exception - // to cascade to and cancel all other routines before finally bubbling up - // to the main extractor loop. - L.e("MediaStore extraction failed: $e") - incompleteSongs.close( - Exception("MediaStore extraction failed: ${e.stackTraceToString()}")) - return@async - } - incompleteSongs.close() - } - - // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date - // metadata for them, and then forwards it to DeviceLibrary. - L.d("Starting tag extraction") - val tagJob = - scope.async { - try { - tagExtractor.consume(incompleteSongs, completeSongs) - } catch (e: Exception) { - L.e("Tag extraction failed: $e") - completeSongs.close( - Exception("Tag extraction failed: ${e.stackTraceToString()}")) - return@async - } - completeSongs.close() - } - - // DeviceLibrary constructs music parent instances as song information is provided, - // and then forwards them to the primary loading loop. - L.d("Starting DeviceLibrary creation") - val deviceLibraryJob = - scope.async(Dispatchers.Default) { - val deviceLibrary = - try { - deviceLibraryFactory.create( - completeSongs, processedSongs, separators, nameFactory) - } catch (e: Exception) { - L.e("DeviceLibrary creation failed: $e") - processedSongs.close( - Exception("DeviceLibrary creation failed: ${e.stackTraceToString()}")) - return@async Result.failure(e) - } - processedSongs.close() - Result.success(deviceLibrary) - } - - // We could keep track of a total here, but we also need to collate this RawSong information - // for when we write the cache later on in the finalization step. - val rawSongs = LinkedList() - // Use a longer timeout so that dependent components can timeout and throw errors that - // provide more context than if we timed out here. - processedSongs.forEachWithTimeout(DEFAULT_TIMEOUT * 2) { - rawSongs.add(it) - // Since discovery takes up the bulk of the music loading process, we switch to - // indicating a defined amount of loaded songs in comparison to the projected amount - // of songs that were queried. - emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) - } - - withTimeout(DEFAULT_TIMEOUT) { - mediaStoreJob.await() - tagJob.await() - } - - // Deliberately done after the involved initialization step to make it less likely - // that the short-circuit occurs so quickly as to break the UI. - // TODO: Do not error, instead just wipe the entire library. - if (rawSongs.isEmpty()) { - L.e("Music library was empty") - throw NoMusicException() - } - - // Now that the library is effectively loaded, we can start the finalization step, which - // involves writing new cache information and creating more music data that is derived - // from the library (e.g playlists) - L.d("Discovered ${rawSongs.size} songs, starting finalization") - - // We have no idea how long the cache will take, and the playlist construction - // will be too fast to indicate, so switch back to an indeterminate state. - emitIndexingProgress(IndexingProgress.Indeterminate) - - // The UserLibrary job is split into a query and construction step, a la MediaStore. - // This way, we can start working on playlists even as DeviceLibrary might still be - // working on parent information. - L.d("Starting UserLibrary query") - val userLibraryQueryJob = - scope.async { - val rawPlaylists = - try { - userLibraryFactory.query() - } catch (e: Exception) { - return@async Result.failure(e) - } - Result.success(rawPlaylists) - } - - // The cache might not exist, or we might have encountered a song not present in it. - // Both situations require us to rewrite the cache in bulk. This is also done parallel - // since the playlist read will probably take some time. - // TODO: Read/write from the cache incrementally instead of in bulk? - if (cache == null || cache.invalidated) { - L.d("Writing cache [why=${cache?.invalidated}]") - cacheRepository.writeCache(rawSongs) - } - - // Create UserLibrary once we finally get the required components for it. - L.d("Awaiting UserLibrary query") - val rawPlaylists = userLibraryQueryJob.await().getOrThrow() - L.d("Awaiting DeviceLibrary creation") - val deviceLibrary = deviceLibraryJob.await().getOrThrow() - L.d("Starting UserLibrary creation") - val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory) - - // Loading process is functionally done, indicate such - L.d( - "Successfully indexed music library [device=$deviceLibrary " + - "user=$userLibrary time=${System.currentTimeMillis() - start}]") - emitIndexingCompletion(null) + val (deviceLibrary, userLibrary) = indexer.run(listOf(), separators, nameFactory) val deviceLibraryChanged: Boolean val userLibraryChanged: Boolean 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 cbd4a272f..4287c5aa2 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 @@ -20,10 +20,8 @@ package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri -import android.provider.OpenableColumns import java.util.UUID import javax.inject.Inject -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -32,13 +30,9 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.fs.Path -import org.oxycblt.auxio.music.stack.fs.contentResolverSafe -import org.oxycblt.auxio.music.stack.fs.useQuery import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators -import org.oxycblt.auxio.util.forEachWithTimeout -import org.oxycblt.auxio.util.sendWithTimeout +import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.util.unlikelyToBeNull import timber.log.Timber as L @@ -341,7 +335,7 @@ class DeviceLibraryImpl( ) : DeviceLibrary { // Use a mapping to make finding information based on it's UID much faster. private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } -// private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } + // private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } @@ -366,14 +360,15 @@ class DeviceLibraryImpl( override fun findSongByPath(path: Path) = null override fun findSongForUri(context: Context, uri: Uri) = null -// context.contentResolverSafe.useQuery( -// uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> -// cursor.moveToFirst() -// // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a -// // song. Do what we can to hopefully find the song the user wanted to open. -// val displayName = -// cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) -// val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) -// songs.find { it.path.name == displayName && it.size == size } -// } + // context.contentResolverSafe.useQuery( + // uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> + // cursor.moveToFirst() + // // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a + // // song. Do what we can to hopefully find the song the user wanted to open. + // val displayName = + // + // cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + // val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) + // songs.find { it.path.name == displayName && it.size == size } + // } } 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 71f0bea40..17cec9bf2 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 @@ -28,16 +28,13 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.fs.MimeType -import org.oxycblt.auxio.music.stack.fs.toAlbumCoverUri -import org.oxycblt.auxio.music.stack.fs.toAudioUri -import org.oxycblt.auxio.music.stack.fs.toSongCoverUri 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.metadata.Separators import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames +import org.oxycblt.auxio.music.stack.fs.MimeType import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.positiveOrNull import org.oxycblt.auxio.util.toUuidOrNull @@ -76,31 +73,28 @@ class SongImpl( } override val name = nameFactory.parse( - requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" }, + requireNotNull(rawSong.name) { "Invalid raw ${rawSong.file.path}: No title" }, rawSong.sortName) override val track = rawSong.track override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } override val date = rawSong.date - override val uri = - requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri() - override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" } - override val mimeType = - MimeType( - fromExtension = - requireNotNull(rawSong.extensionMimeType) { - "Invalid raw ${rawSong.path}: No mime type" - }, - fromFormat = null) - override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" } + override val uri = rawSong.file.uri + override val path = rawSong.file.path + override val mimeType = MimeType(fromExtension = rawSong.file.mimeType, fromFormat = null) + override val size = + requireNotNull(rawSong.file.size) { "Invalid raw ${rawSong.file.path}: No size" } override val durationMs = - requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" } + requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.file.path}: No duration" } override val replayGainAdjustment = ReplayGainAdjustment( track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) + // TODO: See what we want to do with date added now that we can't get it anymore. override val dateAdded = - requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" } + requireNotNull(rawSong.file.lastModified) { + "Invalid raw ${rawSong.file.path}: No date added" + } private var _album: AlbumImpl? = null override val album: Album @@ -114,18 +108,8 @@ class SongImpl( override val genres: List get() = _genres - override val cover = - rawSong.coverPerceptualHash?.let { - // We were able to confirm that the song had a parsable cover and can be used on - // a per-song basis. Otherwise, just fall back to a per-album cover instead, as - // it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not - // support the cover metadata of a given spec (unlikely). - Cover.Embedded( - requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" } - .toSongCoverUri(), - uri, - it) - } ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri()) + // TODO: Rebuild cover system + override val cover = Cover.External(rawSong.file.uri) /** * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an @@ -184,14 +168,10 @@ class SongImpl( rawAlbum = RawAlbum( - mediaStoreId = - requireNotNull(rawSong.albumMediaStoreId) { - "Invalid raw ${rawSong.path}: No album id" - }, musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), name = requireNotNull(rawSong.albumName) { - "Invalid raw ${rawSong.path}: No album name" + "Invalid raw ${rawSong.file.path}: No album name" }, sortName = rawSong.albumSortName, releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)), 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 edadce46a..7f7461f75 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 @@ -22,9 +22,9 @@ import java.util.UUID import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.music.stack.fs.DeviceFile /** * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. @@ -53,10 +53,6 @@ data class RawSong( var subtitle: String? = null, /** @see Song.date */ var date: Date? = null, - /** @see Song.cover */ - var coverPerceptualHash: String? = null, - /** @see RawAlbum.mediaStoreId */ - var albumMediaStoreId: Long? = null, /** @see RawAlbum.musicBrainzId */ var albumMusicBrainzId: String? = null, /** @see RawAlbum.name */ @@ -87,11 +83,6 @@ data class RawSong( * @author Alexander Capehart (OxygenCobalt) */ data class RawAlbum( - /** - * The ID of the [AlbumImpl]'s grouping, obtained from MediaStore. Note that this ID is highly - * unstable and should only be used for accessing the system-provided cover art. - */ - val mediaStoreId: Long, /** @see Music.uid */ override val musicBrainzId: UUID?, /** @see Music.name */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 8ac512f77..0ce6d7df2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -27,12 +27,12 @@ import java.io.InputStreamReader import java.io.OutputStream import javax.inject.Inject import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.music.stack.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.fs.Components import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.music.stack.fs.Volume import org.oxycblt.auxio.music.stack.fs.VolumeManager -import org.oxycblt.auxio.music.stack.extractor.correctWhitespace -import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.unlikelyToBeNull import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index bd9f1856d..20f126396 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -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)) } } 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 082db839c..4df060013 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 @@ -22,19 +22,10 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor -import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractorImpl -import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2 -import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2Impl @Module @InstallIn(SingletonComponent::class) interface MetadataModule { - @Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter - - @Binds fun tagInterpreter2(interpreter: TagInterpreter2Impl): TagInterpreter2 - - @Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor - - @Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory + @Binds + fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt index 9713faede..471b0f21c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * Separators.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 . + */ + package org.oxycblt.auxio.music.metadata import org.oxycblt.auxio.music.stack.extractor.correctWhitespace diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt index 981a977c3..d8342544d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt @@ -1,6 +1,25 @@ +/* + * Copyright (c) 2024 Auxio Project + * Indexer.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 . + */ + package org.oxycblt.auxio.music.stack import android.net.Uri +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -14,7 +33,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.toList import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.info.Name @@ -24,44 +42,51 @@ import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor import org.oxycblt.auxio.music.stack.extractor.TagResult import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.fs.DeviceFiles +import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.UserLibrary -import javax.inject.Inject interface Indexer { - suspend fun run(uris: List, separators: Separators, nameFactory: Name.Known.Factory): LibraryResult + suspend fun run( + uris: List, + separators: Separators, + nameFactory: Name.Known.Factory + ): LibraryResult } -data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: UserLibrary) +data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: MutableUserLibrary) -class IndexerImpl @Inject constructor( +class IndexerImpl +@Inject +constructor( private val deviceFiles: DeviceFiles, private val tagCache: TagCache, private val tagExtractor: ExoPlayerTagExtractor, private val deviceLibraryFactory: DeviceLibrary.Factory, private val userLibraryFactory: UserLibrary.Factory ) : Indexer { - override suspend fun run(uris: List, separators: Separators, nameFactory: Name.Known.Factory) = coroutineScope { - val deviceFiles = deviceFiles.explore(uris.asFlow()) - .flowOn(Dispatchers.IO) - .buffer() - val tagRead = tagCache.read(deviceFiles) - .flowOn(Dispatchers.IO) - .buffer() + override suspend fun run( + uris: List, + separators: Separators, + nameFactory: Name.Known.Factory + ) = coroutineScope { + val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer() + val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer() val (cacheFiles, cacheSongs) = tagRead.split() - val tagExtractor = - tagExtractor.process(cacheFiles) - .flowOn(Dispatchers.IO) - .buffer() + val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer() val (_, extractorSongs) = tagExtractor.split() - val sharedExtractorSongs = extractorSongs.shareIn( - CoroutineScope(Dispatchers.Main), - started = SharingStarted.WhileSubscribed(), - replay = Int.MAX_VALUE - ) - val tagWrite = async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) } + val sharedExtractorSongs = + extractorSongs.shareIn( + CoroutineScope(Dispatchers.Main), + started = SharingStarted.WhileSubscribed(), + replay = Int.MAX_VALUE) + val tagWrite = + async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) } val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() } - val deviceLibrary = deviceLibraryFactory.create(merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory) - val userLibrary = userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory) + val deviceLibrary = + deviceLibraryFactory.create( + merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory) + val userLibrary = + userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory) tagWrite.await() LibraryResult(deviceLibrary, userLibrary) } @@ -71,4 +96,4 @@ class IndexerImpl @Inject constructor( val songs = filterIsInstance().map { it.rawSong } return files to songs } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/StackModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/StackModule.kt new file mode 100644 index 000000000..c2f06b674 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/StackModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Auxio Project + * StackModule.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 . + */ + +package org.oxycblt.auxio.music.stack + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface StackModule { + @Binds fun indexer(impl: IndexerImpl): Indexer +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/CacheModule.kt index 794c0098d..edcf5cb2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/CacheModule.kt @@ -40,8 +40,7 @@ class TagDatabaseModule { @Singleton @Provides fun database(@ApplicationContext context: Context) = - Room.databaseBuilder( - context.applicationContext, TagDatabase::class.java, "music_cache.db") + Room.databaseBuilder(context.applicationContext, TagDatabase::class.java, "music_cache.db") .fallbackToDestructiveMigration() .build() diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt index 93a50b156..874e418e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt @@ -1,20 +1,37 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagCache.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 . + */ + package org.oxycblt.auxio.music.stack.cache +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.transform import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.extractor.TagResult -import javax.inject.Inject +import org.oxycblt.auxio.music.stack.fs.DeviceFile interface TagCache { fun read(files: Flow): Flow + suspend fun write(rawSongs: Flow) } -class TagCacheImpl @Inject constructor( - private val tagDao: TagDao -) : TagCache { +class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache { override fun read(files: Flow) = files.transform { file -> val tags = tagDao.selectTags(file.uri.toString(), file.lastModified) @@ -28,8 +45,6 @@ class TagCacheImpl @Inject constructor( } override suspend fun write(rawSongs: Flow) { - rawSongs.collect { rawSong -> - tagDao.updateTags(Tags.fromRaw(rawSong)) - } + rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt index 92b0c736f..fc52a9806 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * CacheDatabase.kt is part of Auxio. + * TagDatabase.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 @@ -43,16 +43,15 @@ interface TagDao { @Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified") suspend fun selectTags(uri: String, dateModified: Long): Tags? - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun updateTags(tags: Tags) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(tags: Tags) } @Entity @TypeConverters(Tags.Converters::class) data class Tags( /** - * The Uri of the [RawSong]'s audio file, obtained from SAF. - * This should ideally be a black box only used for comparison. + * The Uri of the [RawSong]'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 [RawSong]'s audio file was modified, as a unix epoch timestamp. */ @@ -77,8 +76,6 @@ data class Tags( var subtitle: String? = null, /** @see RawSong.date */ var date: Date? = null, - /** @see RawSong.coverPerceptualHash */ - var coverPerceptualHash: String? = null, /** @see RawSong.albumMusicBrainzId */ var albumMusicBrainzId: String? = null, /** @see RawSong.albumName */ @@ -117,8 +114,6 @@ data class Tags( rawSong.subtitle = subtitle rawSong.date = date - rawSong.coverPerceptualHash = coverPerceptualHash - rawSong.albumMusicBrainzId = albumMusicBrainzId rawSong.albumName = albumName rawSong.albumSortName = albumSortName @@ -163,7 +158,6 @@ data class Tags( disc = rawSong.disc, subtitle = rawSong.subtitle, date = rawSong.date, - coverPerceptualHash = rawSong.coverPerceptualHash, albumMusicBrainzId = rawSong.albumMusicBrainzId, albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" }, albumSortName = rawSong.albumSortName, diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagExtractor2.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt similarity index 70% rename from app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagExtractor2.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt index 9e8037021..a45917284 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagExtractor2.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExoPlayerTagExtractor.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 . + */ + package org.oxycblt.auxio.music.stack.extractor import android.os.HandlerThread @@ -5,17 +23,18 @@ import androidx.media3.common.MediaItem import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray +import java.util.concurrent.Future +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.stack.fs.DeviceFile -import java.util.concurrent.Future -import javax.inject.Inject import timber.log.Timber as L interface TagResult { class Hit(val rawSong: RawSong) : TagResult + class Miss(val file: DeviceFile) : TagResult } @@ -23,27 +42,23 @@ interface ExoPlayerTagExtractor { fun process(deviceFiles: Flow): Flow } -class ExoPlayerTagExtractorImpl @Inject constructor( +class ExoPlayerTagExtractorImpl +@Inject +constructor( private val mediaSourceFactory: MediaSource.Factory, - private val tagInterpreter2: TagInterpreter2, + private val tagInterpreter2: TagInterpreter, ) : ExoPlayerTagExtractor { override fun process(deviceFiles: Flow) = flow { val threadPool = ThreadPool(8, Handler(this)) - deviceFiles.collect { file -> - threadPool.enqueue(file) - } + deviceFiles.collect { file -> threadPool.enqueue(file) } threadPool.empty() } - private inner class Handler( - private val collector: FlowCollector - ) : ThreadPool.Handler { + private inner class Handler(private val collector: FlowCollector) : + ThreadPool.Handler { override suspend fun produce(thread: HandlerThread, input: DeviceFile) = MetadataRetriever.retrieveMetadata( - mediaSourceFactory, - MediaItem.fromUri(input.uri), - thread - ) + mediaSourceFactory, MediaItem.fromUri(input.uri), thread) override suspend fun consume(input: DeviceFile, output: TrackGroupArray) { if (output.isEmpty) { @@ -76,10 +91,7 @@ class ExoPlayerTagExtractorImpl @Inject constructor( private class ThreadPool(size: Int, private val handler: Handler) { private val slots = Array>(size) { - Slot( - thread = HandlerThread("Auxio:ThreadPool:$it"), - task = null - ) + Slot(thread = HandlerThread("Auxio:ThreadPool:$it"), task = null) } suspend fun enqueue(input: I) { @@ -114,25 +126,19 @@ private class ThreadPool(size: Int, private val handler: Handler) { // In-practice this should never block, as all clients // check if the future is done before calling this function. // If you don't maintain that invariant, this will explode. - @Suppress("BlockingMethodInNonBlockingContext") - handler.consume(input, future.get()) + @Suppress("BlockingMethodInNonBlockingContext") handler.consume(input, future.get()) } catch (e: Exception) { L.e("Failed to complete task for $input, ${e.stackTraceToString()}") } } - private data class Slot( - val thread: HandlerThread, - var task: Task? - ) + private data class Slot(val thread: HandlerThread, var task: Task?) - private data class Task( - val input: I, - val future: Future - ) + private data class Task(val input: I, val future: Future) interface Handler { suspend fun produce(thread: HandlerThread, input: I): Future + suspend fun consume(input: I, output: O) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExtractorModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExtractorModule.kt new file mode 100644 index 000000000..97c29e912 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExtractorModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExtractorModule.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 . + */ + +package org.oxycblt.auxio.music.stack.extractor + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface MetadataModule { + @Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter + + @Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter2.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt similarity index 82% rename from app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter2.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt index 61481e653..1619f20b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter2.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt @@ -15,13 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.stack.extractor import androidx.core.text.isDigitsOnly import androidx.media3.exoplayer.MetadataRetriever import javax.inject.Inject -import org.oxycblt.auxio.image.extractor.CoverExtractor import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.util.nonZeroOrNull @@ -32,7 +31,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull * * @author Alexander Capehart (OxygenCobalt) */ -interface TagInterpreter2 { +interface TagInterpreter { /** * Poll to see if this worker is done processing. * @@ -41,8 +40,7 @@ interface TagInterpreter2 { fun interpretOn(textTags: TextTags, rawSong: RawSong) } -class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverExtractor) : - TagInterpreter2 { +class TagInterpreterImpl @Inject constructor() : TagInterpreter { override fun interpretOn(textTags: TextTags, rawSong: RawSong) { populateWithId3v2(rawSong, textTags.id3v2) populateWithVorbis(rawSong, textTags.vorbis) @@ -51,7 +49,7 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE private fun populateWithId3v2(rawSong: RawSong, textFrames: Map>) { // Song (textFrames["TXXX:musicbrainz release track id"] - ?: textFrames["TXXX:musicbrainz_releasetrackid"]) + ?: textFrames["TXXX:musicbrainz_releasetrackid"]) ?.let { rawSong.musicBrainzId = it.first() } textFrames["TIT2"]?.let { rawSong.name = it.first() } textFrames["TSOT"]?.let { rawSong.sortName = it.first() } @@ -76,9 +74,9 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE // TODO: Handle dates that are in "January" because the actual specific release date // isn't known? (textFrames["TDOR"]?.run { Date.from(first()) } - ?: textFrames["TDRC"]?.run { Date.from(first()) } - ?: textFrames["TDRL"]?.run { Date.from(first()) } - ?: parseId3v23Date(textFrames)) + ?: textFrames["TDRC"]?.run { Date.from(first()) } + ?: textFrames["TDRL"]?.run { Date.from(first()) } + ?: parseId3v23Date(textFrames)) ?.let { rawSong.date = it } // Album @@ -88,10 +86,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } (textFrames["TXXX:musicbrainz album type"] - ?: textFrames["TXXX:releasetype"] - ?: - // This is a non-standard iTunes extension - textFrames["GRP1"]) + ?: textFrames["TXXX:releasetype"] + ?: + // This is a non-standard iTunes extension + textFrames["GRP1"]) ?.let { rawSong.releaseTypes = it } // Artist @@ -102,31 +100,31 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE rawSong.artistNames = it } (textFrames["TXXX:artistssort"] - ?: textFrames["TXXX:artists_sort"] - ?: textFrames["TXXX:artists sort"] - ?: textFrames["TSOP"] - ?: textFrames["artistsort"] - ?: textFrames["TXXX:artist sort"]) + ?: textFrames["TXXX:artists_sort"] + ?: textFrames["TXXX:artists sort"] + ?: textFrames["TSOP"] + ?: textFrames["artistsort"] + ?: textFrames["TXXX:artist sort"]) ?.let { rawSong.artistSortNames = it } // Album artist (textFrames["TXXX:musicbrainz album artist id"] - ?: textFrames["TXXX:musicbrainz_albumartistid"]) + ?: textFrames["TXXX:musicbrainz_albumartistid"]) ?.let { rawSong.albumArtistMusicBrainzIds = it } (textFrames["TXXX:albumartists"] - ?: textFrames["TXXX:album_artists"] - ?: textFrames["TXXX:album artists"] - ?: textFrames["TPE2"] - ?: textFrames["TXXX:albumartist"] - ?: textFrames["TXXX:album artist"]) + ?: textFrames["TXXX:album_artists"] + ?: textFrames["TXXX:album artists"] + ?: textFrames["TPE2"] + ?: textFrames["TXXX:albumartist"] + ?: textFrames["TXXX:album artist"]) ?.let { rawSong.albumArtistNames = it } (textFrames["TXXX:albumartistssort"] - ?: textFrames["TXXX:albumartists_sort"] - ?: textFrames["TXXX:albumartists sort"] - ?: textFrames["TXXX:albumartistsort"] - // This is a non-standard iTunes extension - ?: textFrames["TSO2"] - ?: textFrames["TXXX:album artist sort"]) + ?: textFrames["TXXX:albumartists_sort"] + ?: textFrames["TXXX:albumartists sort"] + ?: textFrames["TXXX:albumartistsort"] + // This is a non-standard iTunes extension + ?: textFrames["TSO2"] + ?: textFrames["TXXX:album artist sort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre @@ -197,16 +195,14 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE // Track. parseVorbisPositionField( - comments["tracknumber"]?.first(), - (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first() - ) + comments["tracknumber"]?.first(), + (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first()) ?.let { rawSong.track = it } // Disc and it's subtitle name. parseVorbisPositionField( - comments["discnumber"]?.first(), - (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first() - ) + comments["discnumber"]?.first(), + (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first()) ?.let { rawSong.disc = it } comments["discsubtitle"]?.let { rawSong.subtitle = it.first() } @@ -217,8 +213,8 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE // 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!) (comments["originaldate"]?.run { Date.from(first()) } - ?: comments["date"]?.run { Date.from(first()) } - ?: comments["year"]?.run { Date.from(first()) }) + ?: comments["date"]?.run { Date.from(first()) } + ?: comments["year"]?.run { Date.from(first()) }) ?.let { rawSong.date = it } // Album @@ -237,10 +233,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artistssort"] - ?: comments["artists_sort"] - ?: comments["artists sort"] - ?: comments["artistsort"] - ?: comments["artist sort"]) + ?: comments["artists_sort"] + ?: comments["artists sort"] + ?: comments["artistsort"] + ?: comments["artist sort"]) ?.let { rawSong.artistSortNames = it } // Album artist @@ -248,16 +244,16 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE rawSong.albumArtistMusicBrainzIds = it } (comments["albumartists"] - ?: comments["album_artists"] - ?: comments["album artists"] - ?: comments["albumartist"] - ?: comments["album artist"]) + ?: comments["album_artists"] + ?: comments["album artists"] + ?: comments["albumartist"] + ?: comments["album artist"]) ?.let { rawSong.albumArtistNames = it } (comments["albumartistssort"] - ?: comments["albumartists_sort"] - ?: comments["albumartists sort"] - ?: comments["albumartistsort"] - ?: comments["album artist sort"]) + ?: comments["albumartists_sort"] + ?: comments["albumartists sort"] + ?: comments["albumartistsort"] + ?: comments["album artist sort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre @@ -281,10 +277,10 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE // normally the only tag used for opus files, but some software still writes replay gain // tags anyway. (comments["r128_track_gain"]?.parseR128Adjustment() - ?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment()) + ?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment()) ?.let { rawSong.replayGainTrackAdjustment = it } (comments["r128_album_gain"]?.parseR128Adjustment() - ?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment()) + ?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment()) ?.let { rawSong.replayGainAlbumAdjustment = it } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt index 090d8634c..9ac4e65f5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt @@ -37,8 +37,12 @@ interface DeviceFiles { } @OptIn(ExperimentalCoroutinesApi::class) -class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) : - DeviceFiles { +class DeviceFilesImpl +@Inject +constructor( + @ApplicationContext private val context: Context, + private val volumeManager: VolumeManager +) : DeviceFiles { private val contentResolver = context.contentResolverSafe override fun explore(uris: Flow): Flow = @@ -49,6 +53,9 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex uri: Uri, relativePath: Components ): Flow = flow { + // TODO: Temporary to maintain path api parity + // Figure out what we actually want to do to paths now in saf world. + val external = volumeManager.getInternalVolume() contentResolver.useQuery(uri, PROJECTION) { cursor -> val childUriIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) @@ -77,7 +84,7 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex // rather than just being a glorified async. val lastModified = cursor.getLong(lastModifiedIndex) val size = cursor.getLong(sizeIndex) - emit(DeviceFile(childUri, mimeType, path, size, lastModified)) + emit(DeviceFile(childUri, mimeType, Path(external, path), size, lastModified)) } } // Hypothetically, we could just emitAll as we recurse into a new directory, @@ -96,9 +103,14 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED - ) + DocumentsContract.Document.COLUMN_LAST_MODIFIED) } } -data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components, val size: Long, val lastModified: Long) +data class DeviceFile( + val uri: Uri, + val mimeType: String, + val path: Path, + val size: Long, + val lastModified: Long +) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt index c6cebda61..b45abe0c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt @@ -51,4 +51,6 @@ class FsModule { interface FsBindsModule { @Binds fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory + + @Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles }