music: reorganize metadata/tag/model structure

This commit is contained in:
Alexander Capehart 2024-12-02 14:22:38 -07:00
parent 59652b2f9b
commit 7582c8c9cf
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
56 changed files with 287 additions and 383 deletions

View file

@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
import org.oxycblt.auxio.musikr.model.ReleaseType import org.oxycblt.auxio.musikr.tag.ReleaseType
import timber.log.Timber as L import timber.log.Timber as L
interface DetailGenerator { interface DetailGenerator {

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.detail.list.SongPropertyAdapter
import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.metadata.AudioProperties import org.oxycblt.auxio.musikr.metadata.AudioProperties
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
import org.oxycblt.auxio.musikr.model.resolveNumber import org.oxycblt.auxio.musikr.tag.resolveNumber
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat

View file

@ -29,10 +29,10 @@ import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.musikr.cover.Cover
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.model.ReleaseType import org.oxycblt.auxio.musikr.tag.ReleaseType
import org.oxycblt.auxio.musikr.fs.MimeType import org.oxycblt.auxio.musikr.fs.MimeType
import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.fs.Path
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment

View file

@ -25,12 +25,12 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.interpret.Separators import org.oxycblt.auxio.musikr.tag.interpret.Separators
import org.oxycblt.auxio.musikr.Indexer import org.oxycblt.auxio.musikr.Indexer
import org.oxycblt.auxio.musikr.IndexingProgress import org.oxycblt.auxio.musikr.IndexingProgress
import org.oxycblt.auxio.musikr.interpret.Interpretation import org.oxycblt.auxio.musikr.tag.Interpretation
import org.oxycblt.auxio.musikr.model.MutableLibrary import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
import timber.log.Timber as L import timber.log.Timber as L
/** /**

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.musikr.interpret.Separators import org.oxycblt.auxio.musikr.tag.interpret.Separators
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import timber.log.Timber as L import timber.log.Timber as L

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.resolveNumber import org.oxycblt.auxio.musikr.tag.resolveNumber
import org.oxycblt.auxio.search.SearchEngine import org.oxycblt.auxio.search.SearchEngine
class MusicBrowser class MusicBrowser

View file

@ -28,9 +28,9 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import org.oxycblt.auxio.musikr.explore.Explorer import org.oxycblt.auxio.musikr.explore.Explorer
import org.oxycblt.auxio.musikr.interpret.Interpretation import org.oxycblt.auxio.musikr.tag.Interpretation
import org.oxycblt.auxio.musikr.interpret.Modeler import org.oxycblt.auxio.musikr.model.Modeler
import org.oxycblt.auxio.musikr.model.MutableLibrary import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
interface Indexer { interface Indexer {
suspend fun run( suspend fun run(

View file

@ -24,15 +24,10 @@ import org.oxycblt.auxio.music.Song
sealed interface Cover { sealed interface Cover {
val key: String val key: String
class Single(song: Song) : Cover { data class Single(override val key: String) : Cover
override val key = "${song.uid}@${song.lastModified}"
val uid = song.uid
val uri = song.uri
val lastModified = song.lastModified
}
class Multi(val all: List<Single>) : Cover { class Multi(val all: List<Single>) : Cover {
override val key = "multi@${all.map { it.key }.hashCode()}" override val key = "multi@${all.hashCode()}"
} }
companion object { companion object {
@ -40,15 +35,16 @@ sealed interface Cover {
fun nil() = Multi(listOf()) fun nil() = Multi(listOf())
fun single(song: Song) = Single(song) fun single(key: String) = Single(key)
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) } fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
private fun order(songs: Collection<Song>) = private fun order(songs: Collection<Song>) =
FALLBACK_SORT.songs(songs) FALLBACK_SORT.songs(songs)
.groupBy { it.album } .map { it.cover }
.groupBy { it.key }
.entries .entries
.sortedByDescending { it.value.size } .sortedByDescending { it.value.size }
.map { it.value.first().cover } .map { it.value.first() }
} }
} }

View file

@ -1,23 +0,0 @@
/*
* 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.interpret
import org.oxycblt.auxio.musikr.model.Name
data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)

View file

@ -1,11 +1,12 @@
package org.oxycblt.auxio.musikr.metadata package org.oxycblt.auxio.musikr.metadata
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import androidx.media3.common.Format
import androidx.media3.common.Metadata import androidx.media3.common.Metadata
import org.oxycblt.auxio.musikr.fs.DeviceFile import org.oxycblt.auxio.musikr.fs.DeviceFile
data class AudioMetadata( data class AudioMetadata(
val file: DeviceFile, val file: DeviceFile,
val exoPlayerMetadata: Metadata, val exoPlayerFormat: Format,
val mediaMetadataRetriever: MediaMetadataRetriever val mediaMetadataRetriever: MediaMetadataRetriever
) )

View file

@ -1,8 +1,53 @@
package org.oxycblt.auxio.musikr.metadata package org.oxycblt.auxio.musikr.metadata
import android.content.Context
import android.media.MediaMetadataRetriever
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.MetadataRetriever
import androidx.media3.exoplayer.source.MediaSource
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow 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 org.oxycblt.auxio.musikr.fs.DeviceFile
import javax.inject.Inject
interface MetadataExtractor { interface MetadataExtractor {
fun extract(files: Flow<DeviceFile>): Flow<AudioMetadata> fun extract(files: Flow<DeviceFile>): Flow<AudioMetadata>
}
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)
}
}
val trackGroupArray = exoPlayerMetadataFuture.await()
if (trackGroupArray.isEmpty) {
return@mapNotNull null
}
val trackGroup = trackGroupArray.get(0)
if (trackGroup.length == 0) {
return@mapNotNull null
}
val format = trackGroup.getFormat(0)
AudioMetadata(
it,
format,
mediaMetadataRetriever
)
}
} }

View file

@ -28,4 +28,7 @@ import dagger.hilt.components.SingletonComponent
interface MetadataModule { interface MetadataModule {
@Binds @Binds
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
@Binds
fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor
} }

View file

@ -1,25 +1,5 @@
/* package org.oxycblt.auxio.musikr.metadata
* Copyright (c) 2023 Auxio Project
* TagExtractor.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.extractor
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
@ -38,101 +18,9 @@ import androidx.media3.exoplayer.upstream.Allocator
import androidx.media3.exoplayer.upstream.DefaultAllocator import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber
import java.util.concurrent.Future
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.guava.asDeferred
import org.oxycblt.auxio.musikr.tag.AudioFile
import org.oxycblt.auxio.musikr.fs.DeviceFile
import timber.log.Timber as L
interface TagExtractor {
fun extract(deviceFiles: Flow<DeviceFile>): Flow<AudioFile>
}
class TagExtractorImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
private val mediaSourceFactory: MediaSource.Factory,
) : TagExtractor {
override fun extract(deviceFiles: Flow<DeviceFile>) = flow {
val retriever = ChunkedMetadataRetriever(mediaSourceFactory)
deviceFiles.collect { deviceFile ->
// val exoPlayerMetadataFuture =
// MetadataRetriever.retrieveMetadata(
// mediaSourceFactory, MediaItem.fromUri(deviceFile.uri))
val exoPlayerMetadataFuture = retriever.retrieve(deviceFile.uri)
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(context, deviceFile.uri)
val exoPlayerMetadata = exoPlayerMetadataFuture.asDeferred().await()
val result = extractTags(deviceFile, exoPlayerMetadata, mediaMetadataRetriever)
mediaMetadataRetriever.close()
emit(result)
}
retriever.release()
}
private fun extractTags(
input: DeviceFile,
output: TrackGroupArray,
retriever: MediaMetadataRetriever
): AudioFile {
if (output.isEmpty) return defaultAudioFile(input, retriever)
val track = output.get(0)
if (track.length == 0) return defaultAudioFile(input, retriever)
val format = track.getFormat(0)
val metadata = format.metadata ?: return defaultAudioFile(input, retriever)
val textTags = TextTags(metadata)
return AudioFile(
deviceFile = input,
durationMs =
need(
retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong(),
"duration"),
replayGainTrackAdjustment = textTags.replayGainTrackAdjustment(),
replayGainAlbumAdjustment = textTags.replayGainAlbumAdjustment(),
musicBrainzId = textTags.musicBrainzId(),
name = need(textTags.name() ?: input.path.name, "name"),
sortName = textTags.sortName(),
track = textTags.track(),
disc = textTags.disc(),
subtitle = textTags.subtitle(),
date = textTags.date(),
albumMusicBrainzId = textTags.albumMusicBrainzId(),
albumName = textTags.albumName(),
albumSortName = textTags.albumSortName(),
releaseTypes = textTags.releaseTypes() ?: listOf(),
artistMusicBrainzIds = textTags.artistMusicBrainzIds() ?: listOf(),
artistNames = textTags.artistNames() ?: listOf(),
artistSortNames = textTags.artistSortNames() ?: listOf(),
albumArtistMusicBrainzIds = textTags.albumArtistMusicBrainzIds() ?: listOf(),
albumArtistNames = textTags.albumArtistNames() ?: listOf(),
albumArtistSortNames = textTags.albumArtistSortNames() ?: listOf(),
genreNames = textTags.genreNames() ?: listOf())
}
private fun defaultAudioFile(
deviceFile: DeviceFile,
metadataRetriever: MediaMetadataRetriever
) =
AudioFile(
deviceFile,
name = need(deviceFile.path.name, "name"),
durationMs =
need(
metadataRetriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong(),
"duration"),
)
private fun <T> need(a: T, called: String) =
requireNotNull(a) { "Invalid tag, missing $called" }
}
private const val MESSAGE_PREPARE = 0 private const val MESSAGE_PREPARE = 0
private const val MESSAGE_CONTINUE_LOADING = 1 private const val MESSAGE_CONTINUE_LOADING = 1
@ -140,19 +28,24 @@ private const val MESSAGE_CHECK_FAILURE = 2
private const val MESSAGE_RELEASE = 3 private const val MESSAGE_RELEASE = 3
private const val CHECK_INTERVAL_MS = 100 private const val CHECK_INTERVAL_MS = 100
/** // TODO: Rewrite and re-integrate
* Patched version of Media3's MetadataRetriever that extracts metadata from several tracks at once
* on one thread. This is generally more efficient than stacking several threads at once. interface MetadataRetrieverExt {
* fun retrieveMetadata(mediaItem: MediaItem): Future<TrackGroupArray>
* @author Media3 Team, Alexander Capehart (OxygenCobalt) fun retrieve()
*/
private class ChunkedMetadataRetriever(private val mediaSourceFactory: MediaSource.Factory) : interface Factory {
Handler.Callback { fun create(): MetadataRetrieverExt
}
}
class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) :
MetadataRetrieverExt, Handler.Callback {
private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}") private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}")
private val mediaSourceHandler: HandlerWrapper private val mediaSourceHandler: HandlerWrapper
private var job: MetadataJob? = null private var job: MetadataJob? = null
private data class JobParams(val uri: Uri, val future: SettableFuture<TrackGroupArray>) private data class JobParams(val mediaItem: MediaItem, val future: SettableFuture<TrackGroupArray>)
private class JobData( private class JobData(
val params: JobParams, val params: JobParams,
@ -167,15 +60,15 @@ private class ChunkedMetadataRetriever(private val mediaSourceFactory: MediaSour
mediaSourceHandler = Clock.DEFAULT.createHandler(mediaSourceThread.looper, this) mediaSourceHandler = Clock.DEFAULT.createHandler(mediaSourceThread.looper, this)
} }
fun retrieve(uri: Uri): ListenableFuture<TrackGroupArray> { override fun retrieveMetadata(mediaItem: MediaItem): Future<TrackGroupArray> {
val job = job val job = job
check(job == null || job.data.params.future.isDone) { "Already working on something: $job" } check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
val future = SettableFuture.create<TrackGroupArray>() val future = SettableFuture.create<TrackGroupArray>()
mediaSourceHandler.obtainMessage(MESSAGE_PREPARE, JobParams(uri, future)).sendToTarget() mediaSourceHandler.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future)).sendToTarget()
return future return future
} }
fun release() { override fun retrieve() {
mediaSourceHandler.removeCallbacksAndMessages(null) mediaSourceHandler.removeCallbacksAndMessages(null)
mediaSourceThread.quit() mediaSourceThread.quit()
} }
@ -186,7 +79,7 @@ private class ChunkedMetadataRetriever(private val mediaSourceFactory: MediaSour
val params = msg.obj as JobParams val params = msg.obj as JobParams
val mediaSource = val mediaSource =
mediaSourceFactory.createMediaSource(MediaItem.fromUri(params.uri)) mediaSourceFactory.createMediaSource(params.mediaItem)
val data = JobData(params, mediaSource, null) val data = JobData(params, mediaSource, null)
val mediaSourceCaller = MediaSourceCaller(data) val mediaSourceCaller = MediaSourceCaller(data)
mediaSource.prepareSource( mediaSource.prepareSource(
@ -217,8 +110,8 @@ private class ChunkedMetadataRetriever(private val mediaSourceFactory: MediaSour
mediaPeriod.maybeThrowPrepareError() mediaPeriod.maybeThrowPrepareError()
} }
} catch (e: Exception) { } catch (e: Exception) {
L.e("Failed to extract MediaSource") Timber.e("Failed to extract MediaSource")
L.e(e.stackTraceToString()) Timber.e(e.stackTraceToString())
mediaPeriod?.let(mediaSource::releasePeriod) mediaPeriod?.let(mediaSource::releasePeriod)
mediaSource.releaseSource(mediaSourceCaller) mediaSource.releaseSource(mediaSourceCaller)
job.data.params.future.setException(e) job.data.params.future.setException(e)

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2023 Auxio Project
* PrepareModule.kt is part of Auxio. * ModelModule.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.prepare package org.oxycblt.auxio.musikr.model
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -25,6 +25,6 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface PrepareModule { interface InterpretModule {
@Binds fun prepare(factory: PreparerImpl): Preparer @Binds fun interpreter(interpreter: ModelerImpl): Modeler
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* Interpreter.kt is part of Auxio. * Modeler.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret package org.oxycblt.auxio.musikr.model
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -30,19 +30,20 @@ import kotlinx.coroutines.flow.toList
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.musikr.tag.AudioFile import org.oxycblt.auxio.musikr.tag.AudioFile
import org.oxycblt.auxio.musikr.playlist.PlaylistFile import org.oxycblt.auxio.musikr.playlist.PlaylistFile
import org.oxycblt.auxio.musikr.interpret.link.AlbumLinker import org.oxycblt.auxio.musikr.model.graph.AlbumLinker
import org.oxycblt.auxio.musikr.interpret.link.ArtistLinker import org.oxycblt.auxio.musikr.model.graph.ArtistLinker
import org.oxycblt.auxio.musikr.interpret.link.GenreLinker import org.oxycblt.auxio.musikr.model.graph.GenreLinker
import org.oxycblt.auxio.musikr.interpret.link.Linked import org.oxycblt.auxio.musikr.model.graph.Linked
import org.oxycblt.auxio.musikr.interpret.link.LinkedSong import org.oxycblt.auxio.musikr.model.graph.LinkedSong
import org.oxycblt.auxio.musikr.model.AlbumImpl import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
import org.oxycblt.auxio.musikr.model.ArtistImpl import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
import org.oxycblt.auxio.musikr.model.GenreImpl import org.oxycblt.auxio.musikr.model.impl.GenreImpl
import org.oxycblt.auxio.musikr.model.LibraryImpl import org.oxycblt.auxio.musikr.model.impl.LibraryImpl
import org.oxycblt.auxio.musikr.model.MutableLibrary import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
import org.oxycblt.auxio.musikr.model.SongImpl import org.oxycblt.auxio.musikr.model.impl.SongImpl
import org.oxycblt.auxio.musikr.interpret.prepare.PreSong import org.oxycblt.auxio.musikr.tag.Interpretation
import org.oxycblt.auxio.musikr.interpret.prepare.Preparer 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 as L
interface Modeler { interface Modeler {
@ -53,14 +54,14 @@ interface Modeler {
): MutableLibrary ): MutableLibrary
} }
class ModelerImpl @Inject constructor(private val preparer: Preparer) : Modeler { class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter) : Modeler {
override suspend fun model( override suspend fun model(
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation interpretation: Interpretation
): MutableLibrary { ): MutableLibrary {
val preSongs = val preSongs =
preparer tagInterpreter
.interpret(audioFiles, interpretation) .interpret(audioFiles, interpretation)
.flowOn(Dispatchers.Main) .flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)

View file

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.musikr.model.AlbumImpl import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
import org.oxycblt.auxio.musikr.model.SongImpl import org.oxycblt.auxio.musikr.model.impl.SongImpl
class AlbumLinker { class AlbumLinker {
private val tree = mutableMapOf<String?, MutableMap<UUID?, AlbumLink>>() private val tree = mutableMapOf<String?, MutableMap<UUID?, AlbumLink>>()

View file

@ -16,17 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.musikr.model.AlbumImpl import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
import org.oxycblt.auxio.musikr.model.ArtistImpl import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
import org.oxycblt.auxio.musikr.model.SongImpl import org.oxycblt.auxio.musikr.model.impl.SongImpl
import org.oxycblt.auxio.musikr.interpret.prepare.PreAlbum import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
import org.oxycblt.auxio.musikr.interpret.prepare.PreArtist import org.oxycblt.auxio.musikr.tag.interpret.PreArtist
class ArtistLinker { class ArtistLinker {
private val tree = mutableMapOf<String?, MutableMap<UUID?, ArtistLink>>() private val tree = mutableMapOf<String?, MutableMap<UUID?, ArtistLink>>()

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
class Contribution<T> { class Contribution<T> {
private val map = mutableMapOf<T, Int>() private val map = mutableMapOf<T, Int>()

View file

@ -16,14 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.musikr.model.GenreImpl import org.oxycblt.auxio.musikr.model.impl.GenreImpl
import org.oxycblt.auxio.musikr.model.SongImpl import org.oxycblt.auxio.musikr.model.impl.SongImpl
import org.oxycblt.auxio.musikr.interpret.prepare.PreGenre import org.oxycblt.auxio.musikr.tag.interpret.PreGenre
import org.oxycblt.auxio.musikr.interpret.prepare.PreSong import org.oxycblt.auxio.musikr.tag.interpret.PreSong
class GenreLinker { class GenreLinker {
private val tree = mutableMapOf<String?, GenreLink>() private val tree = mutableMapOf<String?, GenreLink>()

View file

@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
import org.oxycblt.auxio.musikr.model.AlbumImpl import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
import org.oxycblt.auxio.musikr.model.ArtistImpl import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
import org.oxycblt.auxio.musikr.model.GenreImpl import org.oxycblt.auxio.musikr.model.impl.GenreImpl
import org.oxycblt.auxio.musikr.model.PlaylistImpl import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
import org.oxycblt.auxio.musikr.model.SongImpl import org.oxycblt.auxio.musikr.model.impl.SongImpl
import org.oxycblt.auxio.musikr.interpret.prepare.PreAlbum import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
import org.oxycblt.auxio.musikr.interpret.prepare.PrePlaylist import org.oxycblt.auxio.musikr.tag.interpret.PrePlaylist
import org.oxycblt.auxio.musikr.interpret.prepare.PreSong import org.oxycblt.auxio.musikr.tag.interpret.PreSong
interface LinkedSong { interface LinkedSong {
val preSong: PreSong val preSong: PreSong

View file

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.link package org.oxycblt.auxio.musikr.model.graph
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.oxycblt.auxio.musikr.playlist.PlaylistFile import org.oxycblt.auxio.musikr.playlist.PlaylistFile
import org.oxycblt.auxio.musikr.model.PlaylistImpl import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
class PlaylistLinker { class PlaylistLinker {
fun register( fun register(

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.model.impl
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.musikr.cover.Cover
@ -27,10 +27,11 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.interpret.link.LinkedAlbum import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum
import org.oxycblt.auxio.musikr.interpret.link.LinkedSong import org.oxycblt.auxio.musikr.model.graph.LinkedSong
import org.oxycblt.auxio.musikr.interpret.prepare.PreArtist import org.oxycblt.auxio.musikr.tag.Date
import org.oxycblt.auxio.musikr.interpret.prepare.PreGenre import org.oxycblt.auxio.musikr.tag.interpret.PreArtist
import org.oxycblt.auxio.musikr.tag.interpret.PreGenre
import org.oxycblt.auxio.util.update import org.oxycblt.auxio.util.update
/** /**

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.model.impl
import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music

View file

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.model.impl
import org.oxycblt.auxio.musikr.cover.Cover import org.oxycblt.auxio.musikr.cover.Cover
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.musikr.interpret.link.LinkedPlaylist import org.oxycblt.auxio.musikr.model.graph.LinkedPlaylist
import org.oxycblt.auxio.musikr.tag.Name
class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist { class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist {
private val prePlaylist = linkedPlaylist.prePlaylist private val prePlaylist = linkedPlaylist.prePlaylist

View file

@ -28,7 +28,7 @@ import java.io.OutputStream
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.musikr.tag.extractor.correctWhitespace import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
import org.oxycblt.auxio.musikr.fs.Components import org.oxycblt.auxio.musikr.fs.Components
import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.fs.Path
import org.oxycblt.auxio.musikr.fs.Volume import org.oxycblt.auxio.musikr.fs.Volume

View file

@ -1,6 +1,5 @@
package org.oxycblt.auxio.musikr.tag package org.oxycblt.auxio.musikr.tag
import org.oxycblt.auxio.musikr.model.Date
import org.oxycblt.auxio.musikr.fs.DeviceFile import org.oxycblt.auxio.musikr.fs.DeviceFile
data class AudioFile( data class AudioFile(

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.tag
import android.content.Context import android.content.Context
import java.text.ParseException import java.text.ParseException

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.tag
import android.content.Context import android.content.Context
import org.oxycblt.auxio.R import org.oxycblt.auxio.R

View file

@ -0,0 +1,5 @@
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)

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.tag
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.model package org.oxycblt.auxio.musikr.tag
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.musikr.model.ReleaseType.Album import org.oxycblt.auxio.musikr.tag.ReleaseType.Album
/** /**
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc. * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.

View file

@ -49,5 +49,5 @@ class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
} }
override fun write(rawSongs: Flow<AudioFile>) = override fun write(rawSongs: Flow<AudioFile>) =
rawSongs.onEach { file -> tagDao.updateTags(Tags.fromAudioFile(file)) } rawSongs.onEach { file -> tagDao.updateTags(CachedTags.fromAudioFile(file)) }
} }

View file

@ -28,28 +28,28 @@ import androidx.room.Query
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
import org.oxycblt.auxio.musikr.tag.AudioFile import org.oxycblt.auxio.musikr.tag.AudioFile
import org.oxycblt.auxio.musikr.fs.DeviceFile import org.oxycblt.auxio.musikr.fs.DeviceFile
import org.oxycblt.auxio.musikr.tag.extractor.correctWhitespace import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
import org.oxycblt.auxio.musikr.tag.extractor.splitEscaped import org.oxycblt.auxio.musikr.tag.util.splitEscaped
@Database(entities = [Tags::class], version = 50, exportSchema = false) @Database(entities = [CachedTags::class], version = 50, exportSchema = false)
abstract class TagDatabase : RoomDatabase() { abstract class TagDatabase : RoomDatabase() {
abstract fun cachedSongsDao(): TagDao abstract fun cachedSongsDao(): TagDao
} }
@Dao @Dao
interface TagDao { interface TagDao {
@Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified") @Query("SELECT * FROM CachedTags WHERE uri = :uri AND dateModified = :dateModified")
suspend fun selectTags(uri: String, dateModified: Long): Tags? suspend fun selectTags(uri: String, dateModified: Long): CachedTags?
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(tags: Tags) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(cachedTags: CachedTags)
} }
@Entity @Entity
@TypeConverters(Tags.Converters::class) @TypeConverters(CachedTags.Converters::class)
data class Tags( data class CachedTags(
/** /**
* The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black
* box only used for comparison. * box only used for comparison.
@ -140,7 +140,7 @@ data class Tags(
companion object { companion object {
fun fromAudioFile(audioFile: AudioFile) = fun fromAudioFile(audioFile: AudioFile) =
Tags( CachedTags(
uri = audioFile.deviceFile.uri.toString(), uri = audioFile.deviceFile.uri.toString(),
dateModified = audioFile.deviceFile.lastModified, dateModified = audioFile.deviceFile.lastModified,
musicBrainzId = audioFile.musicBrainzId, musicBrainzId = audioFile.musicBrainzId,

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* ExtractorModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.musikr.tag.extractor
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface MetadataModule {
@Binds fun tagExtractor(impl: TagExtractorImpl): TagExtractor
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2024 Auxio Project
* InterpretModule.kt is part of Auxio. * ModelModule.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret package org.oxycblt.auxio.musikr.tag.interpret
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -26,5 +26,5 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface InterpretModule { interface InterpretModule {
@Binds fun interpreter(interpreter: ModelerImpl): Modeler @Binds fun tagInterpreter(factory: TagInterpreterImpl): TagInterpreter
} }

View file

@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.prepare package org.oxycblt.auxio.musikr.tag.interpret
import android.net.Uri import android.net.Uri
import java.util.UUID import java.util.UUID
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.model.ReleaseType import org.oxycblt.auxio.musikr.tag.ReleaseType
import org.oxycblt.auxio.musikr.playlist.PlaylistHandle import org.oxycblt.auxio.musikr.playlist.PlaylistHandle
import org.oxycblt.auxio.musikr.fs.MimeType import org.oxycblt.auxio.musikr.fs.MimeType
import org.oxycblt.auxio.musikr.fs.Path import org.oxycblt.auxio.musikr.fs.Path

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret package org.oxycblt.auxio.musikr.tag.interpret
import org.oxycblt.auxio.musikr.tag.extractor.correctWhitespace import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
import org.oxycblt.auxio.musikr.tag.extractor.splitEscaped import org.oxycblt.auxio.musikr.tag.util.splitEscaped
/** /**
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags * Defines the user-specified parsing of multi-value tags. This should be used to parse any tags

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* Preparer.kt is part of Auxio. * TagInterpreter.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,26 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.prepare package org.oxycblt.auxio.musikr.tag.interpret
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.model.ReleaseType import org.oxycblt.auxio.musikr.tag.ReleaseType
import org.oxycblt.auxio.musikr.tag.AudioFile import org.oxycblt.auxio.musikr.tag.AudioFile
import org.oxycblt.auxio.musikr.fs.MimeType import org.oxycblt.auxio.musikr.fs.MimeType
import org.oxycblt.auxio.musikr.interpret.Interpretation import org.oxycblt.auxio.musikr.tag.Interpretation
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
interface Preparer { interface TagInterpreter {
fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong> fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
} }
class PreparerImpl @Inject constructor() : Preparer { class TagInterpreterImpl @Inject constructor() : TagInterpreter {
override fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation) = override fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation) =
audioFiles.map { audioFile -> audioFiles.map { audioFile ->
val individualPreArtists = val individualPreArtists =

View file

@ -0,0 +1,8 @@
package org.oxycblt.auxio.musikr.tag.parse
import org.oxycblt.auxio.musikr.fs.DeviceFile
data class ParsedTags(
val deviceFile: DeviceFile,
)

View file

@ -16,10 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.tag.extractor package org.oxycblt.auxio.musikr.tag.parse
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
import org.oxycblt.auxio.musikr.tag.util.parseId3v2PositionField
import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
// Song // Song
@ -234,9 +236,6 @@ private fun List<String>.parseR128Adjustment() =
private fun List<String>.parseReplayGainAdjustment() = private fun List<String>.parseReplayGainAdjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
val COMPILATION_RELEASE_TYPES = listOf("compilation")
/** /**
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music: * Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla * https://github.com/vanilla-music/vanilla

View file

@ -0,0 +1,4 @@
package org.oxycblt.auxio.musikr.tag.parse
interface TagParser {
}

View file

@ -16,12 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.tag.extractor package org.oxycblt.auxio.musikr.tag.parse
import androidx.media3.common.Metadata import androidx.media3.common.Metadata
import androidx.media3.extractor.metadata.id3.InternalFrame import androidx.media3.extractor.metadata.id3.InternalFrame
import androidx.media3.extractor.metadata.id3.TextInformationFrame import androidx.media3.extractor.metadata.id3.TextInformationFrame
import androidx.media3.extractor.metadata.vorbis.VorbisComment import androidx.media3.extractor.metadata.vorbis.VorbisComment
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
/** /**
* Processing wrapper for [Metadata] that allows organized access to text-based audio tags. * Processing wrapper for [Metadata] that allows organized access to text-based audio tags.

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* ID3Genre.kt is part of Auxio. * ID3.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.interpret.prepare package org.oxycblt.auxio.musikr.tag.util
/// --- ID3v2 PARSING --- /// --- ID3v2 PARSING ---

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * Copyright (c) 2022 Auxio Project
* TagUtil.kt is part of Auxio. * Transform.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,17 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.musikr.tag.extractor package org.oxycblt.auxio.musikr.tag.util
import org.oxycblt.auxio.util.positiveOrNull
/// --- GENERIC PARSING --- /// --- GENERIC PARSING ---
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag. // TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have
// to deal with the cross-module dependencies of MediaStoreExtractor.
/** /**
* Split a [String] by the given selector, automatically handling escaped characters that satisfy * Split a [String] by the given selector, automatically handling escaped characters that satisfy
* the selector. * the selector.
@ -86,49 +81,3 @@ fun String.correctWhitespace() = trim().ifBlank { null }
* @return A list of non-blank strings with trailing whitespace removed. * @return A list of non-blank strings with trailing whitespace removed.
*/ */
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() } fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/**
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
* (optional) total value delimited by a /.
*
* @return The position value extracted from the string field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun String.parseId3v2PositionField() =
split('/', limit = 2).let {
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
}
/**
* Parse a vorbis-style position + total field. These fields consist of two fields for the position
* and total numbers.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun parseVorbisPositionField(pos: String?, total: String?) =
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
/**
* Transform a raw position + total field into a position a way that tolerates placeholder values.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*/
fun transformPositionField(pos: Int?, total: Int?) =
if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) {
pos
} else {
null
}

View file

@ -0,0 +1,50 @@
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 /.
*
* @return The position value extracted from the string field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun String.parseId3v2PositionField() =
split('/', limit = 2).let {
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
}
/**
* Parse a vorbis-style position + total field. These fields consist of two fields for the position
* and total numbers.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun parseVorbisPositionField(pos: String?, total: String?) =
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
/**
* Transform a raw position + total field into a position a way that tolerates placeholder values.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*/
fun transformPositionField(pos: Int?, total: Int?) =
if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) {
pos
} else {
null
}

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MediaSessionUID
import org.oxycblt.auxio.music.service.MusicBrowser import org.oxycblt.auxio.music.service.MusicBrowser
import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackCommand

View file

@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import timber.log.Timber as L import timber.log.Timber as L
/** /**

View file

@ -22,7 +22,7 @@ import java.security.MessageDigest
import java.util.UUID import java.util.UUID
import kotlin.reflect.KClass import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
/** /**
* Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull], * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull],

View file

@ -21,7 +21,7 @@ package org.oxycblt.auxio.music.info
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.model.Date import org.oxycblt.auxio.musikr.tag.Date
class DateTest { class DateTest {
@Test @Test

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.info
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.model.Disc import org.oxycblt.auxio.musikr.tag.Disc
class DiscTest { class DiscTest {
@Test @Test

View file

@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.info
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.model.Name import org.oxycblt.auxio.musikr.tag.Name
import org.oxycblt.auxio.musikr.model.SortToken import org.oxycblt.auxio.musikr.tag.SortToken
class NameTest { class NameTest {
@Test @Test

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.info
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.model.ReleaseType import org.oxycblt.auxio.musikr.tag.ReleaseType
class ReleaseTypeTest { class ReleaseTypeTest {
@Test @Test

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.metadata
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.interpret.Separators import org.oxycblt.auxio.musikr.tag.interpret.Separators
class SeparatorsTest { class SeparatorsTest {
@Test @Test

View file

@ -20,11 +20,11 @@ package org.oxycblt.auxio.music.metadata
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.tag.extractor.correctWhitespace import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
import org.oxycblt.auxio.musikr.tag.extractor.parseId3v2PositionField import org.oxycblt.auxio.musikr.tag.util.parseId3v2PositionField
import org.oxycblt.auxio.musikr.tag.extractor.parseVorbisPositionField import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
import org.oxycblt.auxio.musikr.tag.extractor.splitEscaped import org.oxycblt.auxio.musikr.tag.util.splitEscaped
import org.oxycblt.auxio.musikr.interpret.prepare.parseId3GenreNames import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
class TagUtilTest { class TagUtilTest {
@Test @Test

View file

@ -27,9 +27,9 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.musikr.tag.extractor.TextTags import org.oxycblt.auxio.musikr.tag.parse.TextTags
class TextTagsTest { class TextCachedTagsTest {
@Test @Test
fun textTags_vorbis() { fun textTags_vorbis() {
val textTags = TextTags(VORBIS_METADATA) val textTags = TextTags(VORBIS_METADATA)