diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 6d0eee90e..2329343e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -34,9 +34,9 @@ import org.oxycblt.auxio.detail.list.SongPropertyAdapter import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.musikr.tag.Name -import org.oxycblt.auxio.musikr.metadata.AudioProperties import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.musikr.metadata.AudioProperties +import org.oxycblt.auxio.musikr.tag.Name import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.replaygain.formatDb import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 0ddfc80a3..37dbbaeb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -65,8 +65,8 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage -import org.oxycblt.auxio.musikr.playlist.m3u.M3U import org.oxycblt.auxio.musikr.IndexingProgress +import org.oxycblt.auxio.musikr.playlist.m3u.M3U import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt index ba2f632f1..9a8e7629a 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt @@ -43,8 +43,8 @@ import kotlinx.coroutines.withContext import okio.FileSystem import okio.buffer import okio.source -import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.image.stack.CoverRetriever +import org.oxycblt.auxio.musikr.cover.Cover class CoverKeyer @Inject constructor() : Keyer { override fun key(data: Cover, options: Options) = "${data.key}&${options.size}" diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt index e292f8ebf..b7bebc407 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt @@ -20,9 +20,9 @@ package org.oxycblt.auxio.image.stack import java.io.InputStream import javax.inject.Inject +import org.oxycblt.auxio.image.stack.extractor.CoverExtractor import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.musikr.cover.CoverCache -import org.oxycblt.auxio.image.stack.extractor.CoverExtractor import timber.log.Timber interface CoverRetriever { diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt index 22a3bead7..08adb6a58 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt @@ -35,6 +35,7 @@ interface CoverSource { class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) : CoverExtractor { override suspend fun extract(cover: Cover.Single): ByteArray? { + return null for (coverSource in coverSources.sources) { val stream = coverSource.extract(cover.uri) if (stream != null) { 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 80cf5a223..57e4a9085 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -27,14 +27,14 @@ import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.musikr.cover.Cover +import org.oxycblt.auxio.musikr.fs.MimeType +import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.tag.Date import org.oxycblt.auxio.musikr.tag.Disc import org.oxycblt.auxio.musikr.tag.Name import org.oxycblt.auxio.musikr.tag.ReleaseType -import org.oxycblt.auxio.musikr.fs.MimeType -import org.oxycblt.auxio.musikr.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 86de79bdf..bcf29ca6e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -25,12 +25,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.music.MusicRepository.IndexingWorker -import org.oxycblt.auxio.musikr.tag.Name -import org.oxycblt.auxio.musikr.tag.interpret.Separators import org.oxycblt.auxio.musikr.Indexer import org.oxycblt.auxio.musikr.IndexingProgress -import org.oxycblt.auxio.musikr.tag.Interpretation import org.oxycblt.auxio.musikr.model.impl.MutableLibrary +import org.oxycblt.auxio.musikr.tag.Interpretation +import org.oxycblt.auxio.musikr.tag.Name +import org.oxycblt.auxio.musikr.tag.interpret.Separators import timber.log.Timber as L /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index d65a0f986..88bff6582 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -24,7 +24,7 @@ import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R -import org.oxycblt.auxio.musikr.fs.DocumentPathFactory +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory import org.oxycblt.auxio.settings.Settings import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt index fb1e61580..137f0c2b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicLocationsBinding import org.oxycblt.auxio.music.MusicSettings -import org.oxycblt.auxio.musikr.fs.DocumentPathFactory +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.showToast import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/musikr/Indexer.kt index 67b3ec0d9..896f684ba 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/Indexer.kt @@ -20,17 +20,16 @@ package org.oxycblt.auxio.musikr import android.net.Uri import javax.inject.Inject -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import org.oxycblt.auxio.musikr.explore.Explorer -import org.oxycblt.auxio.musikr.tag.Interpretation -import org.oxycblt.auxio.musikr.model.Modeler import org.oxycblt.auxio.musikr.model.impl.MutableLibrary +import org.oxycblt.auxio.musikr.pipeline.EvaluateStep +import org.oxycblt.auxio.musikr.pipeline.ExploreStep +import org.oxycblt.auxio.musikr.pipeline.ExtractStep +import org.oxycblt.auxio.musikr.tag.Interpretation interface Indexer { suspend fun run( @@ -53,22 +52,19 @@ sealed interface IndexingProgress { class IndexerImpl @Inject -constructor(private val explorer: Explorer, private val modeler: Modeler) : Indexer { +constructor( + private val exploreStep: ExploreStep, + private val extractStep: ExtractStep, + private val evaluateStep: EvaluateStep +) : Indexer { override suspend fun run( uris: List, interpretation: Interpretation, onProgress: suspend (IndexingProgress) -> Unit ) = coroutineScope { - val files = explorer.explore(uris, onProgress) - val audioFiles = - files.audios - .cap( - start = { onProgress(IndexingProgress.Songs(0, 0)) }, - end = { onProgress(IndexingProgress.Indeterminate) }) - .flowOn(Dispatchers.IO) - .buffer(Channel.UNLIMITED) - val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED) - modeler.model(audioFiles, playlistFiles, interpretation) + val explored = exploreStep.explore(uris).buffer(Channel.UNLIMITED) + val extracted = extractStep.extract(explored).buffer(Channel.UNLIMITED) + evaluateStep.evaluate(interpretation, extracted) } private fun Flow.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow = diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/cover/Cover.kt b/app/src/main/java/org/oxycblt/auxio/musikr/cover/Cover.kt index 0cb6bdb19..1b477f12b 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/cover/Cover.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/cover/Cover.kt @@ -24,7 +24,14 @@ import org.oxycblt.auxio.music.Song sealed interface Cover { val key: String - data class Single(override val key: String) : Cover + data class Single(val song: Song) : Cover { + override val key: String + get() = "${song.uid}@${song.lastModified}" + + val uid = song.uid + val uri = song.uri + val lastModified = song.lastModified + } class Multi(val all: List) : Cover { override val key = "multi@${all.hashCode()}" @@ -35,7 +42,7 @@ sealed interface Cover { fun nil() = Multi(listOf()) - fun single(key: String) = Single(key) + fun single(song: Song) = Single(song) fun multi(songs: Collection) = order(songs).run { Multi(this) } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/cover/CoverCache.kt b/app/src/main/java/org/oxycblt/auxio/musikr/cover/CoverCache.kt index d5f16d0f5..fd79635db 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/cover/CoverCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/cover/CoverCache.kt @@ -44,8 +44,7 @@ constructor( val id = coverIdentifier.identify(data) coverFiles.write(id, data) storedCoversDao.setStoredCover( - StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id) - ) + StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id)) return coverFiles.read(id) } } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/explore/Explorer.kt b/app/src/main/java/org/oxycblt/auxio/musikr/explore/Explorer.kt deleted file mode 100644 index e87e57a22..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/explore/Explorer.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 . - */ - -package org.oxycblt.auxio.musikr.explore - -import android.net.Uri -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.flattenMerge -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.withIndex -import org.oxycblt.auxio.musikr.IndexingProgress -import org.oxycblt.auxio.musikr.fs.DeviceFile -import org.oxycblt.auxio.musikr.tag.cache.CacheResult -import org.oxycblt.auxio.musikr.tag.cache.TagCache -import org.oxycblt.auxio.musikr.tag.extractor.TagExtractor -import org.oxycblt.auxio.musikr.fs.DeviceFiles -import org.oxycblt.auxio.musikr.playlist.db.StoredPlaylists -import org.oxycblt.auxio.musikr.playlist.PlaylistFile -import org.oxycblt.auxio.musikr.tag.AudioFile -import timber.log.Timber - -interface Explorer { - fun explore(uris: List, onProgress: suspend (IndexingProgress.Songs) -> Unit): Files -} - -data class Files(val audios: Flow, val playlists: Flow) - -class ExplorerImpl -@Inject -constructor( - private val deviceFiles: DeviceFiles, - private val tagCache: TagCache, - private val tagExtractor: TagExtractor, - private val storedPlaylists: StoredPlaylists -) : Explorer { - override fun explore( - uris: List, - onProgress: suspend (IndexingProgress.Songs) -> Unit - ): Files { - var loaded = 0 - var explored = 0 - val deviceFiles = - deviceFiles - .explore(uris.asFlow()) - .onEach { - Timber.d("File explored: $it") - explored++ - onProgress(IndexingProgress.Songs(loaded, explored)) - } - .flowOn(Dispatchers.IO) - .buffer(Channel.UNLIMITED) - val cacheResults = - tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED) - val audioFiles = - cacheResults - .handleMisses { misses -> - val extracted = - misses - .stretch(8) { tagExtractor.extract(it).flowOn(Dispatchers.IO) } - .buffer(Channel.UNLIMITED) - val written = - tagCache.write(extracted).flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED) - written - } - .onEach { - loaded++ - onProgress(IndexingProgress.Songs(loaded, explored)) - Timber.d("File extracted: $it") - } - val playlistFiles = storedPlaylists.read() - return Files(audioFiles, playlistFiles) - } - - /** Temporarily split a flow into 8 parallel threads and then */ - private fun Flow.stretch(n: Int, creator: (Flow) -> Flow): Flow { - val posChannels = Array(n) { Channel(Channel.UNLIMITED) } - val divert: Flow = flow { - withIndex().collect { - val index = it.index % n - posChannels[index].send(it.value) - } - for (channel in posChannels) { - channel.close() - } - } - val handle = - posChannels.map { creator(it.receiveAsFlow()).buffer(Channel.UNLIMITED) }.asFlow() - return merge(divert, handle.flattenMerge()) - } - - private fun Flow.handleMisses( - uncached: (Flow) -> Flow - ): Flow { - val uncachedChannel = Channel() - val cachedChannel = Channel() - val divert: Flow = flow { - collect { - when (it) { - is CacheResult.Hit -> cachedChannel.send(it.audioFile) - is CacheResult.Miss -> uncachedChannel.send(it.deviceFile) - } - } - cachedChannel.close() - uncachedChannel.close() - } - val uncached = uncached(uncachedChannel.receiveAsFlow()) - val cached = cachedChannel.receiveAsFlow() - return merge(divert, uncached, cached) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFiles.kt index 8eaaf6a7d..d269dbb36 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFiles.kt @@ -31,12 +31,21 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory import timber.log.Timber interface DeviceFiles { fun explore(uris: Flow): Flow } +data class DeviceFile( + val uri: Uri, + val mimeType: String, + val path: Path, + val size: Long, + val lastModified: Long +) + @OptIn(ExperimentalCoroutinesApi::class) class DeviceFilesImpl @Inject @@ -64,8 +73,7 @@ constructor( ): Flow = flow { contentResolver.useQuery( DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), - PROJECTION - ) { cursor -> + PROJECTION) { cursor -> val childUriIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) val displayNameIndex = @@ -97,8 +105,7 @@ constructor( mimeType, newPath, size, - lastModified) - ) + lastModified)) } } emitAll(recursive.asFlow().flattenMerge()) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/fs/FsModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/fs/FsModule.kt index 2b0a02360..6424a5380 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/fs/FsModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/fs/FsModule.kt @@ -27,6 +27,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactoryImpl +import org.oxycblt.auxio.musikr.fs.path.MediaStorePathInterpreter import org.oxycblt.auxio.util.getSystemServiceCompat @Module diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DocumentPathFactory.kt b/app/src/main/java/org/oxycblt/auxio/musikr/fs/path/DocumentPathFactory.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/musikr/fs/DocumentPathFactory.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/fs/path/DocumentPathFactory.kt index c30d7969a..4967938a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DocumentPathFactory.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/fs/path/DocumentPathFactory.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.fs +package org.oxycblt.auxio.musikr.fs.path import android.content.ContentUris import android.content.Context @@ -25,6 +25,12 @@ import android.provider.DocumentsContract import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File import javax.inject.Inject +import org.oxycblt.auxio.musikr.fs.Components +import org.oxycblt.auxio.musikr.fs.Path +import org.oxycblt.auxio.musikr.fs.Volume +import org.oxycblt.auxio.musikr.fs.VolumeManager +import org.oxycblt.auxio.musikr.fs.contentResolverSafe +import org.oxycblt.auxio.musikr.fs.useQuery /** * A factory for parsing the reverse-engineered format of the URIs obtained from document picker. diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/fs/MediaStorePathInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/musikr/fs/path/MediaStorePathInterpreter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/musikr/fs/MediaStorePathInterpreter.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/fs/path/MediaStorePathInterpreter.kt index 2b7715505..ebe41f451 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/fs/MediaStorePathInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/fs/path/MediaStorePathInterpreter.kt @@ -16,11 +16,14 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.fs +package org.oxycblt.auxio.musikr.fs.path import android.database.Cursor import android.os.Build import android.provider.MediaStore +import org.oxycblt.auxio.musikr.fs.Components +import org.oxycblt.auxio.musikr.fs.Path +import org.oxycblt.auxio.musikr.fs.VolumeManager import timber.log.Timber as L /** diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioMetadata.kt b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioMetadata.kt index 86a7b041f..bdfb061a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioMetadata.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioMetadata.kt @@ -1,12 +1,27 @@ +/* + * Copyright (c) 2024 Auxio Project + * AudioMetadata.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.musikr.metadata import android.media.MediaMetadataRetriever import androidx.media3.common.Format -import androidx.media3.common.Metadata -import org.oxycblt.auxio.musikr.fs.DeviceFile data class AudioMetadata( - val file: DeviceFile, - val exoPlayerFormat: Format, + val exoPlayerFormat: Format?, val mediaMetadataRetriever: MediaMetadataRetriever ) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioProperties.kt index c3a97e7ea..5fb79950e 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/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/musikr/metadata/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataExtractor.kt index 12b9fc34f..ff151dc49 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataExtractor.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * MetadataExtractor.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.musikr.metadata import android.content.Context @@ -6,48 +24,38 @@ import androidx.media3.common.MediaItem import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.source.MediaSource import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.guava.await import kotlinx.coroutines.withContext import org.oxycblt.auxio.musikr.fs.DeviceFile -import javax.inject.Inject interface MetadataExtractor { - fun extract(files: Flow): Flow + suspend fun extract(file: DeviceFile): AudioMetadata } -class MetadataExtractorImpl @Inject constructor( +class MetadataExtractorImpl +@Inject +constructor( @ApplicationContext private val context: Context, private val mediaSourceFactory: MediaSource.Factory ) : MetadataExtractor { - override fun extract(files: Flow) = files.mapNotNull { - val exoPlayerMetadataFuture = MetadataRetriever.retrieveMetadata( - mediaSourceFactory, - MediaItem.fromUri(it.uri) - ) - val mediaMetadataRetriever = MediaMetadataRetriever().apply { - withContext(Dispatchers.IO) { - setDataSource(context, it.uri) + override suspend fun extract(file: DeviceFile): AudioMetadata { + val exoPlayerMetadataFuture = + MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(file.uri)) + val mediaMetadataRetriever = + MediaMetadataRetriever().apply { + withContext(Dispatchers.IO) { setDataSource(context, file.uri) } } - } val trackGroupArray = exoPlayerMetadataFuture.await() if (trackGroupArray.isEmpty) { - return@mapNotNull null + return AudioMetadata(null, mediaMetadataRetriever) } val trackGroup = trackGroupArray.get(0) if (trackGroup.length == 0) { - return@mapNotNull null + return AudioMetadata(null, mediaMetadataRetriever) } val format = trackGroup.getFormat(0) - AudioMetadata( - it, - format, - mediaMetadataRetriever - ) + return AudioMetadata(format, mediaMetadataRetriever) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataModule.kt index 6f2da9433..459127266 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/MetadataModule.kt @@ -29,6 +29,5 @@ interface MetadataModule { @Binds fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory - @Binds - fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor + @Binds fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/ReusableMetadataRetriever.kt b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/ReusableMetadataRetriever.kt index 3c5891ab1..4f915fa1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/metadata/ReusableMetadataRetriever.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/metadata/ReusableMetadataRetriever.kt @@ -1,6 +1,23 @@ +/* + * Copyright (c) 2024 Auxio Project + * ReusableMetadataRetriever.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.musikr.metadata -import android.net.Uri import android.os.Handler import android.os.HandlerThread import android.os.Message @@ -16,11 +33,10 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.upstream.Allocator import androidx.media3.exoplayer.upstream.DefaultAllocator -import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import timber.log.Timber import java.util.concurrent.Future import javax.inject.Inject +import timber.log.Timber private const val MESSAGE_PREPARE = 0 private const val MESSAGE_CONTINUE_LOADING = 1 @@ -32,6 +48,7 @@ private const val CHECK_INTERVAL_MS = 100 interface MetadataRetrieverExt { fun retrieveMetadata(mediaItem: MediaItem): Future + fun retrieve() interface Factory { @@ -39,13 +56,18 @@ interface MetadataRetrieverExt { } } -class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) : +class ReusableMetadataRetrieverImpl +@Inject +constructor(private val mediaSourceFactory: MediaSource.Factory) : MetadataRetrieverExt, Handler.Callback { private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}") private val mediaSourceHandler: HandlerWrapper private var job: MetadataJob? = null - private data class JobParams(val mediaItem: MediaItem, val future: SettableFuture) + private data class JobParams( + val mediaItem: MediaItem, + val future: SettableFuture + ) private class JobData( val params: JobParams, @@ -64,7 +86,9 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF val job = job check(job == null || job.data.params.future.isDone) { "Already working on something: $job" } val future = SettableFuture.create() - mediaSourceHandler.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future)).sendToTarget() + mediaSourceHandler + .obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future)) + .sendToTarget() return future } @@ -78,8 +102,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF MESSAGE_PREPARE -> { val params = msg.obj as JobParams - val mediaSource = - mediaSourceFactory.createMediaSource(params.mediaItem) + val mediaSource = mediaSourceFactory.createMediaSource(params.mediaItem) val data = JobData(params, mediaSource, null) val mediaSourceCaller = MediaSourceCaller(data) mediaSource.prepareSource( @@ -87,8 +110,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF job = MetadataJob(data, mediaSourceCaller) mediaSourceHandler.sendEmptyMessageDelayed( - MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS - ) + MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt index 77fc73341..44f33bec6 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt @@ -20,8 +20,8 @@ package org.oxycblt.auxio.musikr.model.graph import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow -import org.oxycblt.auxio.musikr.playlist.PlaylistFile import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl +import org.oxycblt.auxio.musikr.playlist.PlaylistFile class PlaylistLinker { fun register( diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt index ae7f5a2f7..53108f23b 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.musikr.model.impl import kotlin.math.min -import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -27,6 +26,7 @@ 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.musikr.cover.Cover import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum import org.oxycblt.auxio.musikr.model.graph.LinkedSong import org.oxycblt.auxio.musikr.tag.Date @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.update class SongImpl(linkedSong: LinkedSong) : Song { private val preSong = linkedSong.preSong - override val uid = preSong.uid + override val uid = preSong.computeUid() override val name = preSong.name override val track = preSong.track override val disc = preSong.disc diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt index 486f58de5..664fd9a63 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt @@ -18,8 +18,8 @@ package org.oxycblt.auxio.musikr.model.impl -import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.musikr.model.graph.LinkedPlaylist import org.oxycblt.auxio.musikr.tag.Name diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/Modeler.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt similarity index 82% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/Modeler.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt index 54e599821..6d4c0d5dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/Modeler.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * Modeler.kt is part of Auxio. + * EvaluateStep.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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.model +package org.oxycblt.auxio.musikr.pipeline import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -24,12 +24,11 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.musikr.tag.AudioFile -import org.oxycblt.auxio.musikr.playlist.PlaylistFile import org.oxycblt.auxio.musikr.model.graph.AlbumLinker import org.oxycblt.auxio.musikr.model.graph.ArtistLinker import org.oxycblt.auxio.musikr.model.graph.GenreLinker @@ -44,25 +43,28 @@ import org.oxycblt.auxio.musikr.model.impl.SongImpl import org.oxycblt.auxio.musikr.tag.Interpretation import org.oxycblt.auxio.musikr.tag.interpret.PreSong import org.oxycblt.auxio.musikr.tag.interpret.TagInterpreter -import timber.log.Timber as L +import timber.log.Timber -interface Modeler { - suspend fun model( - audioFiles: Flow, - playlistFiles: Flow, - interpretation: Interpretation +interface EvaluateStep { + suspend fun evaluate( + interpretation: Interpretation, + extractedMusic: Flow ): MutableLibrary } -class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter) : Modeler { - override suspend fun model( - audioFiles: Flow, - playlistFiles: Flow, - interpretation: Interpretation +class EvaluateStepImpl +@Inject +constructor( + private val tagInterpreter: TagInterpreter, +) : EvaluateStep { + override suspend fun evaluate( + interpretation: Interpretation, + extractedMusic: Flow ): MutableLibrary { val preSongs = - tagInterpreter - .interpret(audioFiles, interpretation) + extractedMusic + .filterIsInstance() + .map { tagInterpreter.interpret(it.file, it.tags, interpretation) } .flowOn(Dispatchers.Main) .buffer(Channel.UNLIMITED) @@ -91,12 +93,12 @@ class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter val uidMap = mutableMapOf() val songs = albumLinkedSongs.mapNotNull { - val uid = it.preSong.uid + val uid = it.preSong.computeUid() val other = uidMap[uid] if (other == null) { SongImpl(it) } else { - L.d("Song @ $uid already exists at ${other.path}, ignoring") + Timber.d("Song @ $uid already exists at ${other.path}, ignoring") null } } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExploreStep.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExploreStep.kt new file mode 100644 index 000000000..31328e581 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExploreStep.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExploreStep.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.musikr.pipeline + +import android.net.Uri +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.fs.DeviceFiles +import org.oxycblt.auxio.musikr.playlist.m3u.M3U + +interface ExploreStep { + fun explore(uris: List): Flow +} + +class ExploreStepImpl @Inject constructor(private val deviceFiles: DeviceFiles) : ExploreStep { + override fun explore(uris: List) = + deviceFiles + .explore(uris.asFlow()) + .mapNotNull { + when { + it.mimeType == M3U.MIME_TYPE -> null + it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it) + else -> null + } + } + .flowOn(Dispatchers.IO) +} + +sealed interface ExploreNode { + data class Audio(val file: DeviceFile) : ExploreNode +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExtractStep.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExtractStep.kt new file mode 100644 index 000000000..2dd032af3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/ExtractStep.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExtractStep.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.musikr.pipeline + +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.metadata.MetadataExtractor +import org.oxycblt.auxio.musikr.tag.cache.TagCache +import org.oxycblt.auxio.musikr.tag.parse.ParsedTags +import org.oxycblt.auxio.musikr.tag.parse.TagParser + +interface ExtractStep { + fun extract(nodes: Flow): Flow +} + +class ExtractStepImpl +@Inject +constructor( + private val tagCache: TagCache, + private val metadataExtractor: MetadataExtractor, + private val tagParser: TagParser +) : ExtractStep { + override fun extract(nodes: Flow): Flow { + val cacheResults = + nodes + .filterIsInstance() + .map { + val tags = tagCache.read(it.file) + MaybeCachedSong(it.file, tags) + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + val (cachedSongs, uncachedSongs) = + cacheResults.mapPartition { + it.tags?.let { tags -> ExtractedMusic.Song(it.file, tags) } + } + val split = uncachedSongs.distribute(8) + val extractedSongs = + Array(split.hot.size) { i -> + split.hot[i] + .map { + val metadata = metadataExtractor.extract(it.file) + val tags = tagParser.parse(it.file, metadata) + ExtractedMusic.Song(it.file, tags) + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + } + return merge( + cachedSongs, + split.cold, + *extractedSongs, + ) + } + + data class MaybeCachedSong(val file: DeviceFile, val tags: ParsedTags?) +} + +sealed interface ExtractedMusic { + data class Song(val file: DeviceFile, val tags: ParsedTags) : ExtractedMusic +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/FlowUtil.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/FlowUtil.kt new file mode 100644 index 000000000..1ad88f942 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/FlowUtil.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Auxio Project + * FlowUtil.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.musikr.pipeline + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.withIndex + +data class HotCold(val hot: H, val cold: Flow) + +inline fun Flow.mapPartition(crossinline predicate: (T) -> R?): HotCold, T> { + val passChannel = Channel(Channel.UNLIMITED) + val passFlow = passChannel.consumeAsFlow() + val failFlow = flow { + collect { + val result = predicate(it) + if (result != null) { + passChannel.send(result) + } else { + emit(it) + } + } + } + return HotCold(passFlow, failFlow) +} + +/** + * Equally "distributes" the values of some flow across n new flows. + * + * Note that this function requires the "manager" flow to be consumed alongside the split flows in + * order to function. Without this, all of the newly split flows will simply block. + */ +fun Flow.distribute(n: Int): HotCold>, Nothing> { + val posChannels = Array(n) { Channel(Channel.UNLIMITED) } + val managerFlow = + flow { + withIndex().collect { + val index = it.index % n + posChannels[index].send(it.value) + } + for (channel in posChannels) { + channel.close() + } + } + val hotFlows = posChannels.map { it.receiveAsFlow() }.toTypedArray() + return HotCold(hotFlows, managerFlow) +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/PipelineModule.kt similarity index 73% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/pipeline/PipelineModule.kt index 9fd2f9ef0..8eafc4fae 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/PipelineModule.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023 Auxio Project - * ModelModule.kt is part of Auxio. + * Copyright (c) 2024 Auxio Project + * PipelineModule.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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.model +package org.oxycblt.auxio.musikr.pipeline import dagger.Binds import dagger.Module @@ -26,5 +26,9 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface InterpretModule { - @Binds fun interpreter(interpreter: ModelerImpl): Modeler + @Binds fun exploreStep(step: ExploreStepImpl): ExploreStep + + @Binds fun extractStep(step: ExtractStepImpl): ExtractStep + + @Binds fun evaluateStep(step: EvaluateStepImpl): EvaluateStep } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/ExternalPlaylistManager.kt index 0e70767ad..6b2d7706b 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/ExternalPlaylistManager.kt @@ -23,11 +23,11 @@ import android.net.Uri import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.musikr.playlist.m3u.M3U import org.oxycblt.auxio.musikr.fs.Components -import org.oxycblt.auxio.musikr.fs.DocumentPathFactory import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.fs.contentResolverSafe +import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory +import org.oxycblt.auxio.musikr.playlist.m3u.M3U import timber.log.Timber as L /** @@ -39,8 +39,7 @@ import timber.log.Timber as L */ interface ExternalPlaylistManager { /** - * Import the playli L.d("Unable to extract bit rate field") -st file at the given [uri]. + * Import the playli L.d("Unable to extract bit rate field") st file at the given [uri]. * * @param uri The [Uri] of the playlist file to import. * @return An [ImportedPlaylist] containing the paths to the files listed in the playlist file, diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistFile.kt b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistFile.kt index c323e9b2a..1133b4df2 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistFile.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistFile.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaylistFile.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.musikr.playlist import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistModule.kt index 1f2795bdb..4df6125f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/PlaylistModule.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * ExternalModule.kt is part of Auxio. + * PlaylistModule.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 @@ -22,8 +22,6 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.musikr.playlist.m3u.M3U -import org.oxycblt.auxio.musikr.playlist.m3u.M3UImpl @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3U.kt b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3U.kt index 4d2e28248..562d4eb0d 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3U.kt @@ -28,7 +28,6 @@ import java.io.OutputStream import javax.inject.Inject import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.musikr.tag.util.correctWhitespace import org.oxycblt.auxio.musikr.fs.Components import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.fs.Volume @@ -36,6 +35,7 @@ import org.oxycblt.auxio.musikr.fs.VolumeManager import org.oxycblt.auxio.musikr.playlist.ExportConfig import org.oxycblt.auxio.musikr.playlist.ImportedPlaylist import org.oxycblt.auxio.musikr.playlist.PossiblePaths +import org.oxycblt.auxio.musikr.tag.util.correctWhitespace import org.oxycblt.auxio.util.unlikelyToBeNull import timber.log.Timber as L @@ -154,8 +154,7 @@ constructor( else -> listOf( InterpretedPath(Components.parseUnix(path), false), - InterpretedPath(Components.parseWindows(path), true) - ) + InterpretedPath(Components.parseWindows(path), true)) } private fun expandInterpretation( diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3UModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3UModule.kt index ae5c455c9..1d8a55035 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3UModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/playlist/m3u/M3UModule.kt @@ -1,13 +1,27 @@ +/* + * Copyright (c) 2024 Auxio Project + * M3UModule.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.musikr.playlist.m3u import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.musikr.playlist.ExternalPlaylistManager -import org.oxycblt.auxio.musikr.playlist.ExternalPlaylistManagerImpl -import org.oxycblt.auxio.musikr.playlist.m3u.M3U -import org.oxycblt.auxio.musikr.playlist.m3u.M3UImpl @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/AudioFile.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/AudioFile.kt deleted file mode 100644 index 731d2f068..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/AudioFile.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.oxycblt.auxio.musikr.tag - -import org.oxycblt.auxio.musikr.fs.DeviceFile - -data class AudioFile( - val deviceFile: DeviceFile, - val durationMs: Long, - val replayGainTrackAdjustment: Float? = null, - val replayGainAlbumAdjustment: Float? = null, - val musicBrainzId: String? = null, - val name: String, - val sortName: String? = null, - val track: Int? = null, - val disc: Int? = null, - val subtitle: String? = null, - val date: Date? = null, - val albumMusicBrainzId: String? = null, - val albumName: String? = null, - val albumSortName: String? = null, - val releaseTypes: List = listOf(), - val artistMusicBrainzIds: List = listOf(), - val artistNames: List = listOf(), - val artistSortNames: List = listOf(), - val albumArtistMusicBrainzIds: List = listOf(), - val albumArtistNames: List = listOf(), - val albumArtistSortNames: List = listOf(), - val genreNames: List = listOf() -) \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/Interpretation.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/Interpretation.kt index 46f2be400..41c7c6afd 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/Interpretation.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/Interpretation.kt @@ -1,5 +1,23 @@ +/* + * 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 . + */ + package org.oxycblt.auxio.musikr.tag import org.oxycblt.auxio.musikr.tag.interpret.Separators -data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators) \ No newline at end of file +data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagCache.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagCache.kt index 041bf637a..b20f9d566 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagCache.kt @@ -19,35 +19,19 @@ package org.oxycblt.auxio.musikr.tag.cache import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import org.oxycblt.auxio.musikr.tag.AudioFile import org.oxycblt.auxio.musikr.fs.DeviceFile - -sealed interface CacheResult { - data class Hit(val audioFile: AudioFile) : CacheResult - - data class Miss(val deviceFile: DeviceFile) : CacheResult -} +import org.oxycblt.auxio.musikr.tag.parse.ParsedTags interface TagCache { - fun read(files: Flow): Flow + suspend fun read(file: DeviceFile): ParsedTags? - fun write(rawSongs: Flow): Flow + suspend fun write(file: DeviceFile, tags: ParsedTags) } class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache { - override fun read(files: Flow) = - files.map { file -> - val tags = tagDao.selectTags(file.uri.toString(), file.lastModified) - if (tags != null) { - CacheResult.Hit(tags.toAudioFile(file)) - } else { - CacheResult.Miss(file) - } - } + override suspend fun read(file: DeviceFile) = + tagDao.selectTags(file.uri.toString(), file.lastModified)?.intoParsedTags() - override fun write(rawSongs: Flow) = - rawSongs.onEach { file -> tagDao.updateTags(CachedTags.fromAudioFile(file)) } + override suspend fun write(file: DeviceFile, tags: ParsedTags) = + tagDao.updateTags(CachedTags.fromParsedTags(file, tags)) } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagDatabase.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagDatabase.kt index c3e279c7d..71c319b1c 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/cache/TagDatabase.kt @@ -28,9 +28,9 @@ import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters -import org.oxycblt.auxio.musikr.tag.Date -import org.oxycblt.auxio.musikr.tag.AudioFile import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.tag.Date +import org.oxycblt.auxio.musikr.tag.parse.ParsedTags import org.oxycblt.auxio.musikr.tag.util.correctWhitespace import org.oxycblt.auxio.musikr.tag.util.splitEscaped @@ -100,9 +100,8 @@ data class CachedTags( /** @see AudioFile.genreNames */ val genreNames: List = listOf() ) { - fun toAudioFile(deviceFile: DeviceFile) = - AudioFile( - deviceFile = deviceFile, + fun intoParsedTags() = + ParsedTags( musicBrainzId = musicBrainzId, name = name, sortName = sortName, @@ -139,30 +138,30 @@ data class CachedTags( } companion object { - fun fromAudioFile(audioFile: AudioFile) = + fun fromParsedTags(deviceFile: DeviceFile, parsedTags: ParsedTags) = CachedTags( - uri = audioFile.deviceFile.uri.toString(), - dateModified = audioFile.deviceFile.lastModified, - musicBrainzId = audioFile.musicBrainzId, - name = audioFile.name, - sortName = audioFile.sortName, - durationMs = audioFile.durationMs, - replayGainTrackAdjustment = audioFile.replayGainTrackAdjustment, - replayGainAlbumAdjustment = audioFile.replayGainAlbumAdjustment, - track = audioFile.track, - disc = audioFile.disc, - subtitle = audioFile.subtitle, - date = audioFile.date, - albumMusicBrainzId = audioFile.albumMusicBrainzId, - albumName = audioFile.albumName, - albumSortName = audioFile.albumSortName, - releaseTypes = audioFile.releaseTypes, - artistMusicBrainzIds = audioFile.artistMusicBrainzIds, - artistNames = audioFile.artistNames, - artistSortNames = audioFile.artistSortNames, - albumArtistMusicBrainzIds = audioFile.albumArtistMusicBrainzIds, - albumArtistNames = audioFile.albumArtistNames, - albumArtistSortNames = audioFile.albumArtistSortNames, - genreNames = audioFile.genreNames) + uri = deviceFile.uri.toString(), + dateModified = deviceFile.lastModified, + musicBrainzId = parsedTags.musicBrainzId, + name = parsedTags.name, + sortName = parsedTags.sortName, + durationMs = parsedTags.durationMs, + replayGainTrackAdjustment = parsedTags.replayGainTrackAdjustment, + replayGainAlbumAdjustment = parsedTags.replayGainAlbumAdjustment, + track = parsedTags.track, + disc = parsedTags.disc, + subtitle = parsedTags.subtitle, + date = parsedTags.date, + albumMusicBrainzId = parsedTags.albumMusicBrainzId, + albumName = parsedTags.albumName, + albumSortName = parsedTags.albumSortName, + releaseTypes = parsedTags.releaseTypes, + artistMusicBrainzIds = parsedTags.artistMusicBrainzIds, + artistNames = parsedTags.artistNames, + artistSortNames = parsedTags.artistSortNames, + albumArtistMusicBrainzIds = parsedTags.albumArtistMusicBrainzIds, + albumArtistNames = parsedTags.albumArtistNames, + albumArtistSortNames = parsedTags.albumArtistSortNames, + genreNames = parsedTags.genreNames) } } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/InterpretModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/InterpretModule.kt index de71e3ba8..5f58cb6e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/InterpretModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/InterpretModule.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * ModelModule.kt is part of Auxio. + * InterpretModule.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 diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/PreMusic.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/PreMusic.kt index 2a25ea888..1c8b37265 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/PreMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/PreMusic.kt @@ -22,13 +22,13 @@ import android.net.Uri import java.util.UUID import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType +import org.oxycblt.auxio.musikr.fs.MimeType +import org.oxycblt.auxio.musikr.fs.Path +import org.oxycblt.auxio.musikr.playlist.PlaylistHandle import org.oxycblt.auxio.musikr.tag.Date import org.oxycblt.auxio.musikr.tag.Disc import org.oxycblt.auxio.musikr.tag.Name import org.oxycblt.auxio.musikr.tag.ReleaseType -import org.oxycblt.auxio.musikr.playlist.PlaylistHandle -import org.oxycblt.auxio.musikr.fs.MimeType -import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.update @@ -51,7 +51,7 @@ data class PreSong( val preArtists: List, val preGenres: List ) { - val uid = + fun computeUid() = musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } ?: Music.UID.auxio(MusicType.SONGS) { // Song UIDs are based on the raw data without parsing so that they remain @@ -77,11 +77,7 @@ data class PreAlbum( val preArtists: List ) -data class PreArtist( - val musicBrainzId: UUID?, - val name: Name, - val rawName: String?, -) +data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?) data class PreGenre( val name: Name, diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/TagInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/TagInterpreter.kt index ef814a7ea..43328ab5b 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/interpret/TagInterpreter.kt @@ -19,91 +19,88 @@ package org.oxycblt.auxio.musikr.tag.interpret import javax.inject.Inject -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.oxycblt.auxio.R +import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.fs.MimeType import org.oxycblt.auxio.musikr.tag.Disc +import org.oxycblt.auxio.musikr.tag.Interpretation import org.oxycblt.auxio.musikr.tag.Name import org.oxycblt.auxio.musikr.tag.ReleaseType -import org.oxycblt.auxio.musikr.tag.AudioFile -import org.oxycblt.auxio.musikr.fs.MimeType -import org.oxycblt.auxio.musikr.tag.Interpretation +import org.oxycblt.auxio.musikr.tag.parse.ParsedTags import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.toUuidOrNull interface TagInterpreter { - fun interpret(audioFiles: Flow, interpretation: Interpretation): Flow + fun interpret(file: DeviceFile, parsedTags: ParsedTags, interpretation: Interpretation): PreSong } class TagInterpreterImpl @Inject constructor() : TagInterpreter { - override fun interpret(audioFiles: Flow, 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, - 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, - ), - lastModified = audioFile.deviceFile.lastModified, - // TODO: Figure out what to do with date added - dateAdded = audioFile.deviceFile.lastModified, - preAlbum = preAlbum, - preArtists = rawArtists, - preGenres = rawGenres) - } - - private fun need(audioFile: AudioFile, what: String, value: T?) = - requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" } + override fun interpret( + file: DeviceFile, + parsedTags: ParsedTags, + interpretation: Interpretation + ): PreSong { + val individualPreArtists = + makePreArtists( + parsedTags.artistMusicBrainzIds, + parsedTags.artistNames, + parsedTags.artistSortNames, + interpretation) + val albumPreArtists = + makePreArtists( + parsedTags.albumArtistMusicBrainzIds, + parsedTags.albumArtistNames, + parsedTags.albumArtistSortNames, + interpretation) + val preAlbum = + makePreAlbum(file, parsedTags, individualPreArtists, albumPreArtists, interpretation) + val rawArtists = + individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } + val rawGenres = + makePreGenres(parsedTags, interpretation).ifEmpty { listOf(unknownPreGenre()) } + val uri = file.uri + return PreSong( + musicBrainzId = parsedTags.musicBrainzId?.toUuidOrNull(), + name = interpretation.nameFactory.parse(parsedTags.name, parsedTags.sortName), + rawName = parsedTags.name, + track = parsedTags.track, + disc = parsedTags.disc?.let { Disc(it, parsedTags.subtitle) }, + date = parsedTags.date, + uri = uri, + path = file.path, + mimeType = MimeType(file.mimeType, null), + size = file.size, + durationMs = parsedTags.durationMs, + replayGainAdjustment = + ReplayGainAdjustment( + parsedTags.replayGainTrackAdjustment, + parsedTags.replayGainAlbumAdjustment, + ), + lastModified = file.lastModified, + // TODO: Figure out what to do with date added + dateAdded = file.lastModified, + preAlbum = preAlbum, + preArtists = rawArtists, + preGenres = rawGenres) + } private fun makePreAlbum( - audioFile: AudioFile, + file: DeviceFile, + parsedTags: ParsedTags, individualPreArtists: List, albumPreArtists: List, interpretation: Interpretation ): PreAlbum { - val rawAlbumName = need(audioFile, "album name", audioFile.albumName) + // TODO: Make fallbacks for this! + val rawAlbumName = requireNotNull(parsedTags.albumName) return PreAlbum( - musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(), - name = interpretation.nameFactory.parse(rawAlbumName, audioFile.albumSortName), + musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(), + name = interpretation.nameFactory.parse(rawAlbumName, parsedTags.albumSortName), rawName = rawAlbumName, releaseType = - ReleaseType.parse(interpretation.separators.split(audioFile.releaseTypes)) + ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes)) ?: ReleaseType.Album(null), preArtists = albumPreArtists @@ -141,12 +138,12 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null) private fun makePreGenres( - audioFile: AudioFile, + parsedTags: ParsedTags, interpretation: Interpretation ): List { val genreNames = - audioFile.genreNames.parseId3GenreNames() - ?: interpretation.separators.split(audioFile.genreNames) + parsedTags.genreNames.parseId3GenreNames() + ?: interpretation.separators.split(parsedTags.genreNames) return genreNames.map { makePreGenre(it, interpretation) } } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagFields.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTagFields.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagFields.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTagFields.kt index 5c92e7f9e..621b422b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagFields.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTagFields.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * TagFields.kt is part of Auxio. + * ExoPlayerTagFields.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 @@ -25,32 +25,32 @@ import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField import org.oxycblt.auxio.util.nonZeroOrNull // Song -fun TextTags.musicBrainzId() = +fun ExoPlayerTags.musicBrainzId() = (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 ExoPlayerTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first() -fun TextTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first() +fun ExoPlayerTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first() // Track. -fun TextTags.track() = +fun ExoPlayerTags.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() = +fun ExoPlayerTags.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 ExoPlayerTags.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 @@ -64,7 +64,7 @@ fun TextTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first() // 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() = +fun ExoPlayerTags.date() = (vorbis["originaldate"]?.run { Date.from(first()) } ?: vorbis["date"]?.run { Date.from(first()) } ?: vorbis["year"]?.run { Date.from(first()) } @@ -82,18 +82,18 @@ fun TextTags.date() = ?: parseId3v23Date()) // Album -fun TextTags.albumMusicBrainzId() = +fun ExoPlayerTags.albumMusicBrainzId() = (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 ExoPlayerTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first() -fun TextTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first() +fun ExoPlayerTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first() -fun TextTags.releaseTypes() = +fun ExoPlayerTags.releaseTypes() = (vorbis["releasetype"] ?: vorbis["musicbrainz album type"] ?: id3v2["TXXX:musicbrainz album type"] @@ -103,20 +103,20 @@ fun TextTags.releaseTypes() = id3v2["GRP1"]) // Artist -fun TextTags.artistMusicBrainzIds() = +fun ExoPlayerTags.artistMusicBrainzIds() = (vorbis["musicbrainz_artistid"] ?: vorbis["musicbrainz artist id"] ?: id3v2["TXXX:musicbrainz artist id"] ?: id3v2["TXXX:musicbrainz_artistid"]) -fun TextTags.artistNames() = +fun ExoPlayerTags.artistNames() = (vorbis["artists"] ?: vorbis["artist"] ?: id3v2["TXXX:artists"] ?: id3v2["TPE1"] ?: id3v2["TXXX:artist"]) -fun TextTags.artistSortNames() = +fun ExoPlayerTags.artistSortNames() = (vorbis["artistssort"] ?: vorbis["artists_sort"] ?: vorbis["artists sort"] @@ -129,13 +129,13 @@ fun TextTags.artistSortNames() = ?: id3v2["artistsort"] ?: id3v2["TXXX:artist sort"]) -fun TextTags.albumArtistMusicBrainzIds() = +fun ExoPlayerTags.albumArtistMusicBrainzIds() = (vorbis["musicbrainz_albumartistid"] ?: vorbis["musicbrainz album artist id"] ?: id3v2["TXXX:musicbrainz album artist id"] ?: id3v2["TXXX:musicbrainz_albumartistid"]) -fun TextTags.albumArtistNames() = +fun ExoPlayerTags.albumArtistNames() = (vorbis["albumartists"] ?: vorbis["album_artists"] ?: vorbis["album artists"] @@ -148,7 +148,7 @@ fun TextTags.albumArtistNames() = ?: id3v2["TXXX:albumartist"] ?: id3v2["TXXX:album artist"]) -fun TextTags.albumArtistSortNames() = +fun ExoPlayerTags.albumArtistSortNames() = (vorbis["albumartistssort"] ?: vorbis["albumartists_sort"] ?: vorbis["albumartists sort"] @@ -163,10 +163,10 @@ fun TextTags.albumArtistSortNames() = ?: id3v2["TXXX:album artist sort"]) // Genre -fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"] +fun ExoPlayerTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"] // Compilation Flag -fun TextTags.isCompilation() = +fun ExoPlayerTags.isCompilation() = (vorbis["compilation"] ?: vorbis["itunescompilation"] ?: id3v2["TCMP"] // This is a non-standard itunes extension @@ -178,17 +178,17 @@ fun TextTags.isCompilation() = } // ReplayGain information -fun TextTags.replayGainTrackAdjustment() = +fun ExoPlayerTags.replayGainTrackAdjustment() = (vorbis["r128_track_gain"]?.parseR128Adjustment() ?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment() ?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()) -fun TextTags.replayGainAlbumAdjustment() = +fun ExoPlayerTags.replayGainAlbumAdjustment() = (vorbis["r128_album_gain"]?.parseR128Adjustment() ?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment() ?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()) -private fun TextTags.parseId3v23Date(): Date? { +private fun ExoPlayerTags.parseId3v23Date(): Date? { // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY // is present. val year = diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TextTags.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTags.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TextTags.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTags.kt index 32d5e0915..a89ca917e 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TextTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ExoPlayerTags.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * TextTags.kt is part of Auxio. + * ExoPlayerTags.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 @@ -30,7 +30,7 @@ import org.oxycblt.auxio.musikr.tag.util.correctWhitespace * @param metadata The [Metadata] to wrap. * @author Alexander Capehart (OxygenCobalt) */ -class TextTags(metadata: Metadata) { +class ExoPlayerTags(metadata: Metadata) { private val _id3v2 = mutableMapOf>() /** The ID3v2 text identification frames found in the file. Can have more than one value. */ val id3v2: Map> diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFile.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/MediaMetadataTagFields.kt similarity index 69% rename from app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFile.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/MediaMetadataTagFields.kt index d19d44522..e3ba0f508 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/fs/DeviceFile.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/MediaMetadataTagFields.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023 Auxio Project - * DeviceFile.kt is part of Auxio. + * Copyright (c) 2024 Auxio Project + * MediaMetadataTagFields.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 @@ -16,15 +16,9 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.fs +package org.oxycblt.auxio.musikr.tag.parse -import android.net.Uri - -data class DeviceFile( - val uri: Uri, - val mimeType: String, - val path: Path, - val size: Long, - val lastModified: Long -) +import android.media.MediaMetadataRetriever +fun MediaMetadataRetriever.durationMs() = + extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/explore/ExploreModule.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParseModule.kt similarity index 84% rename from app/src/main/java/org/oxycblt/auxio/musikr/explore/ExploreModule.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParseModule.kt index 587042e90..c802bf906 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/explore/ExploreModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParseModule.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * ExploreModule.kt is part of Auxio. + * ParseModule.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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.musikr.explore +package org.oxycblt.auxio.musikr.tag.parse import dagger.Binds import dagger.Module @@ -25,6 +25,6 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -interface ExploreModule { - @Binds fun explorer(impl: ExplorerImpl): Explorer +interface ParseModule { + @Binds fun tagParser(factory: TagParserImpl): TagParser } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParsedTags.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParsedTags.kt index 93e59e628..b73ed77a7 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParsedTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/ParsedTags.kt @@ -1,8 +1,45 @@ +/* + * Copyright (c) 2024 Auxio Project + * ParsedTags.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.musikr.tag.parse -import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.tag.Date data class ParsedTags( - val deviceFile: DeviceFile, - -) \ No newline at end of file + val durationMs: Long, + val replayGainTrackAdjustment: Float? = null, + val replayGainAlbumAdjustment: Float? = null, + val musicBrainzId: String? = null, + val name: String, + val sortName: String? = null, + val track: Int? = null, + val disc: Int? = null, + val subtitle: String? = null, + val date: Date? = null, + val albumMusicBrainzId: String? = null, + val albumName: String? = null, + val albumSortName: String? = null, + val releaseTypes: List = listOf(), + val artistMusicBrainzIds: List = listOf(), + val artistNames: List = listOf(), + val artistSortNames: List = listOf(), + val albumArtistMusicBrainzIds: List = listOf(), + val albumArtistNames: List = listOf(), + val albumArtistSortNames: List = listOf(), + val genreNames: List = listOf() +) diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagParser.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagParser.kt index ab75aa52b..e07c67015 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagParser.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/parse/TagParser.kt @@ -1,4 +1,66 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagParser.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.musikr.tag.parse +import javax.inject.Inject +import org.oxycblt.auxio.musikr.fs.DeviceFile +import org.oxycblt.auxio.musikr.metadata.AudioMetadata + interface TagParser { -} \ No newline at end of file + fun parse(file: DeviceFile, metadata: AudioMetadata): ParsedTags +} + +class MissingTagError(what: String) : Error("missing tag: $what") + +class TagParserImpl @Inject constructor() : TagParser { + override fun parse(file: DeviceFile, metadata: AudioMetadata): ParsedTags { + val exoPlayerMetadata = + metadata.exoPlayerFormat?.metadata + ?: return ParsedTags( + durationMs = + metadata.mediaMetadataRetriever.durationMs() + ?: throw MissingTagError("durationMs"), + name = file.path.name ?: throw MissingTagError("name"), + ) + val exoPlayerTags = ExoPlayerTags(exoPlayerMetadata) + return ParsedTags( + durationMs = + metadata.mediaMetadataRetriever.durationMs() ?: throw MissingTagError("durationMs"), + replayGainTrackAdjustment = exoPlayerTags.replayGainTrackAdjustment(), + replayGainAlbumAdjustment = exoPlayerTags.replayGainAlbumAdjustment(), + musicBrainzId = exoPlayerTags.musicBrainzId(), + name = exoPlayerTags.name() ?: file.path.name ?: throw MissingTagError("name"), + sortName = exoPlayerTags.sortName(), + track = exoPlayerTags.track(), + disc = exoPlayerTags.disc(), + subtitle = exoPlayerTags.subtitle(), + date = exoPlayerTags.date(), + albumMusicBrainzId = exoPlayerTags.albumMusicBrainzId(), + albumName = exoPlayerTags.albumName(), + albumSortName = exoPlayerTags.albumSortName(), + releaseTypes = exoPlayerTags.releaseTypes() ?: listOf(), + artistMusicBrainzIds = exoPlayerTags.artistMusicBrainzIds() ?: listOf(), + artistNames = exoPlayerTags.artistNames() ?: listOf(), + artistSortNames = exoPlayerTags.artistSortNames() ?: listOf(), + albumArtistMusicBrainzIds = exoPlayerTags.albumArtistMusicBrainzIds() ?: listOf(), + albumArtistNames = exoPlayerTags.albumArtistNames() ?: listOf(), + albumArtistSortNames = exoPlayerTags.albumArtistSortNames() ?: listOf(), + genreNames = exoPlayerTags.genreNames() ?: listOf()) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/tag/util/Vorbis.kt b/app/src/main/java/org/oxycblt/auxio/musikr/tag/util/Vorbis.kt index 6b6ab395b..02dbc351a 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/tag/util/Vorbis.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/tag/util/Vorbis.kt @@ -1,8 +1,25 @@ +/* + * Copyright (c) 2024 Auxio Project + * Vorbis.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.musikr.tag.util import org.oxycblt.auxio.util.positiveOrNull - /** * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an * (optional) total value delimited by a /. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index cd5c96d75..718c8dc02 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -38,9 +38,9 @@ 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.musikr.tag.Name import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MusicBrowser +import org.oxycblt.auxio.musikr.tag.Name import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt index 6b0064244..daa0e7436 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt @@ -21,10 +21,10 @@ package org.oxycblt.auxio.music.metadata import org.junit.Assert.assertEquals import org.junit.Test import org.oxycblt.auxio.musikr.tag.util.correctWhitespace +import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames import org.oxycblt.auxio.musikr.tag.util.parseId3v2PositionField import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField import org.oxycblt.auxio.musikr.tag.util.splitEscaped -import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames class TagUtilTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextCachedTagsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextCachedTagsTest.kt index 77be4c294..500cd73b4 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextCachedTagsTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextCachedTagsTest.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * TextTagsTest.kt is part of Auxio. + * TextCachedTagsTest.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 @@ -27,66 +27,67 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import org.oxycblt.auxio.musikr.tag.parse.TextTags +import org.oxycblt.auxio.musikr.tag.parse.ExoPlayerTags class TextCachedTagsTest { @Test fun textTags_vorbis() { - val textTags = TextTags(VORBIS_METADATA) - assertTrue(textTags.id3v2.isEmpty()) - assertEquals(listOf("Wheel"), textTags.vorbis["title"]) - assertEquals(listOf("Paraglow"), textTags.vorbis["album"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"]) - assertEquals(listOf("2022"), textTags.vorbis["date"]) - assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) - assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["APIC"]) + val exoPlayerTags = ExoPlayerTags(VORBIS_METADATA) + assertTrue(exoPlayerTags.id3v2.isEmpty()) + assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"]) + assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"]) + assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"]) + assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"]) + assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"]) + assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"]) + assertEquals(null, exoPlayerTags.id3v2["APIC"]) } @Test fun textTags_id3v2() { - val textTags = TextTags(ID3V2_METADATA) - assertTrue(textTags.vorbis.isEmpty()) - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) + val exoPlayerTags = ExoPlayerTags(ID3V2_METADATA) + assertTrue(exoPlayerTags.vorbis.isEmpty()) + assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"]) + assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"]) + assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"]) + assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"]) + assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"]) + assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"]) + assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"]) } @Test fun textTags_mp4() { - val textTags = TextTags(MP4_METADATA) - assertTrue(textTags.vorbis.isEmpty()) - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) + val exoPlayerTags = ExoPlayerTags(MP4_METADATA) + assertTrue(exoPlayerTags.vorbis.isEmpty()) + assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"]) + assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"]) + assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"]) + assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"]) + assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"]) + assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"]) + assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"]) } @Test fun textTags_id3v2_vorbis_combined() { - val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA)) - assertEquals(listOf("Wheel"), textTags.vorbis["title"]) - assertEquals(listOf("Paraglow"), textTags.vorbis["album"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"]) - assertEquals(listOf("2022"), textTags.vorbis["date"]) - assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) - assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) + val exoPlayerTags = + ExoPlayerTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA)) + assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"]) + assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"]) + assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"]) + assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"]) + assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"]) + assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"]) + assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"]) - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(null, textTags.id3v2["APIC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) + assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"]) + assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"]) + assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"]) + assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"]) + assertEquals(null, exoPlayerTags.id3v2["APIC"]) + assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"]) + assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"]) } companion object {