musikr: restructure loader into pipeline

This commit is contained in:
Alexander Capehart 2024-12-04 15:08:49 -07:00
parent 7582c8c9cf
commit 7f7ee94f45
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
52 changed files with 753 additions and 508 deletions

View file

@ -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

View file

@ -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

View file

@ -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<Cover> {
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"

View file

@ -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 {

View file

@ -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) {

View file

@ -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

View file

@ -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
/**

View file

@ -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

View file

@ -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

View file

@ -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<Uri>,
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 <T> Flow<T>.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow<T> =

View file

@ -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<Single>) : 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<Song>) = order(songs).run { Multi(this) }

View file

@ -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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Uri>, onProgress: suspend (IndexingProgress.Songs) -> Unit): Files
}
data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
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<Uri>,
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 <T, R> Flow<T>.stretch(n: Int, creator: (Flow<T>) -> Flow<R>): Flow<R> {
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
val divert: Flow<R> = 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<CacheResult>.handleMisses(
uncached: (Flow<DeviceFile>) -> Flow<AudioFile>
): Flow<AudioFile> {
val uncachedChannel = Channel<DeviceFile>()
val cachedChannel = Channel<AudioFile>()
val divert: Flow<AudioFile> = 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)
}
}

View file

@ -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<Uri>): Flow<DeviceFile>
}
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<DeviceFile> = 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())

View file

@ -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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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.

View file

@ -16,11 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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
/**

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
)

View file

@ -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))
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DeviceFile>): Flow<AudioMetadata>
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<DeviceFile>) = 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)
}
}
}

View file

@ -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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<TrackGroupArray>
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<TrackGroupArray>)
private data class JobParams(
val mediaItem: MediaItem,
val future: SettableFuture<TrackGroupArray>
)
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<TrackGroupArray>()
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
}

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<AudioFile>,
playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation
interface EvaluateStep {
suspend fun evaluate(
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary
}
class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter) : Modeler {
override suspend fun model(
audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation
class EvaluateStepImpl
@Inject
constructor(
private val tagInterpreter: TagInterpreter,
) : EvaluateStep {
override suspend fun evaluate(
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary {
val preSongs =
tagInterpreter
.interpret(audioFiles, interpretation)
extractedMusic
.filterIsInstance<ExtractedMusic.Song>()
.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<Music.UID, SongImpl>()
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Uri>): Flow<ExploreNode>
}
class ExploreStepImpl @Inject constructor(private val deviceFiles: DeviceFiles) : ExploreStep {
override fun explore(uris: List<Uri>) =
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ExploreNode>): Flow<ExtractedMusic>
}
class ExtractStepImpl
@Inject
constructor(
private val tagCache: TagCache,
private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser
) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val cacheResults =
nodes
.filterIsInstance<ExploreNode.Audio>()
.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<ExtractedMusic>(
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<H, C>(val hot: H, val cold: Flow<C>)
inline fun <T, R> Flow<T>.mapPartition(crossinline predicate: (T) -> R?): HotCold<Flow<R>, T> {
val passChannel = Channel<R>(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 <T> Flow<T>.distribute(n: Int): HotCold<Array<Flow<T>>, Nothing> {
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
val managerFlow =
flow<Nothing> {
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)
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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,

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.musikr.playlist
import org.oxycblt.auxio.music.Music

View file

@ -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)

View file

@ -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(

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -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<String> = listOf(),
val artistMusicBrainzIds: List<String> = listOf(),
val artistNames: List<String> = listOf(),
val artistSortNames: List<String> = listOf(),
val albumArtistMusicBrainzIds: List<String> = listOf(),
val albumArtistNames: List<String> = listOf(),
val albumArtistSortNames: List<String> = listOf(),
val genreNames: List<String> = listOf()
)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)

View file

@ -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<DeviceFile>): Flow<CacheResult>
suspend fun read(file: DeviceFile): ParsedTags?
fun write(rawSongs: Flow<AudioFile>): Flow<AudioFile>
suspend fun write(file: DeviceFile, tags: ParsedTags)
}
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
override fun read(files: Flow<DeviceFile>) =
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<AudioFile>) =
rawSongs.onEach { file -> tagDao.updateTags(CachedTags.fromAudioFile(file)) }
override suspend fun write(file: DeviceFile, tags: ParsedTags) =
tagDao.updateTags(CachedTags.fromParsedTags(file, tags))
}

View file

@ -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<String> = 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)
}
}

View file

@ -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

View file

@ -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<PreArtist>,
val preGenres: List<PreGenre>
) {
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<PreArtist>
)
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,

View file

@ -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<AudioFile>, interpretation: Interpretation): Flow<PreSong>
fun interpret(file: DeviceFile, parsedTags: ParsedTags, interpretation: Interpretation): PreSong
}
class TagInterpreterImpl @Inject constructor() : TagInterpreter {
override fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation) =
audioFiles.map { audioFile ->
val individualPreArtists =
makePreArtists(
audioFile.artistMusicBrainzIds,
audioFile.artistNames,
audioFile.artistSortNames,
interpretation)
val albumPreArtists =
makePreArtists(
audioFile.albumArtistMusicBrainzIds,
audioFile.albumArtistNames,
audioFile.albumArtistSortNames,
interpretation)
val preAlbum =
makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation)
val rawArtists =
individualPreArtists
.ifEmpty { albumPreArtists }
.ifEmpty { listOf(unknownPreArtist()) }
val rawGenres =
makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) }
val uri = audioFile.deviceFile.uri
PreSong(
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
name =
interpretation.nameFactory.parse(
need(audioFile, "name", audioFile.name), audioFile.sortName),
rawName = audioFile.name,
track = audioFile.track,
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
date = audioFile.date,
uri = uri,
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 <T> 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<PreArtist>,
albumPreArtists: List<PreArtist>,
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<PreGenre> {
val genreNames =
audioFile.genreNames.parseId3GenreNames()
?: interpretation.separators.split(audioFile.genreNames)
parsedTags.genreNames.parseId3GenreNames()
?: interpretation.separators.split(parsedTags.genreNames)
return genreNames.map { makePreGenre(it, interpretation) }
}

View file

@ -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 =

View file

@ -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<String, MutableList<String>>()
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
val id3v2: Map<String, List<String>>

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
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<String> = listOf(),
val artistMusicBrainzIds: List<String> = listOf(),
val artistNames: List<String> = listOf(),
val artistSortNames: List<String> = listOf(),
val albumArtistMusicBrainzIds: List<String> = listOf(),
val albumArtistNames: List<String> = listOf(),
val albumArtistSortNames: List<String> = listOf(),
val genreNames: List<String> = listOf()
)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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 {
}
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())
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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 /.

View file

@ -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

View file

@ -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

View file

@ -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 {