diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt
deleted file mode 100644
index 976a3233f..000000000
--- a/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * CoverModule.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.cover
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-
-@Module
-@InstallIn(SingletonComponent::class)
-interface CoverModule {
- @Binds fun coverParser(impl: CoverParserImpl): CoverParser
-}
diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt
deleted file mode 100644
index 652f910cc..000000000
--- a/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * CoverParser.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.cover
-
-import androidx.media3.common.MediaMetadata
-import androidx.media3.common.Metadata
-import androidx.media3.extractor.metadata.flac.PictureFrame
-import androidx.media3.extractor.metadata.id3.ApicFrame
-import javax.inject.Inject
-import org.oxycblt.musikr.metadata.AudioMetadata
-
-interface CoverParser {
- suspend fun extract(metadata: AudioMetadata): ByteArray?
-}
-
-class CoverParserImpl @Inject constructor() : CoverParser {
- override suspend fun extract(metadata: AudioMetadata): ByteArray? {
- val exoPlayerMetadata = metadata.exoPlayerFormat?.metadata
- return exoPlayerMetadata?.let(::findCoverDataInMetadata)
- ?: metadata.mediaMetadataRetriever.embeddedPicture
- }
-
- private fun findCoverDataInMetadata(metadata: Metadata): ByteArray? {
- var fallbackPic: ByteArray? = null
-
- for (i in 0 until metadata.length()) {
- // We can only extract pictures from two tags with this method, ID3v2's APIC or
- // Vorbis picture comments.
- val pic: ByteArray?
- val type: Int
-
- when (val entry = metadata.get(i)) {
- is ApicFrame -> {
- pic = entry.pictureData
- type = entry.pictureType
- }
- is PictureFrame -> {
- pic = entry.pictureData
- type = entry.pictureType
- }
- else -> continue
- }
-
- if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
- return pic
- } else if (fallbackPic == null) {
- fallbackPic = pic
- }
- }
-
- return fallbackPic
- }
-}
diff --git a/app/src/main/java/org/oxycblt/musikr/metadata/AudioMetadata.kt b/app/src/main/java/org/oxycblt/musikr/metadata/AudioMetadata.kt
deleted file mode 100644
index 07ce984d2..000000000
--- a/app/src/main/java/org/oxycblt/musikr/metadata/AudioMetadata.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * AudioMetadata.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.metadata
-
-import android.media.MediaMetadataRetriever
-import androidx.media3.common.Format
-
-data class AudioMetadata(
- val exoPlayerFormat: Format?,
- val mediaMetadataRetriever: MediaMetadataRetriever
-)
diff --git a/app/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
index a5fe97f71..4bce9ff12 100644
--- a/app/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
+++ b/app/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
@@ -19,43 +19,24 @@
package org.oxycblt.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 javax.inject.Inject
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.guava.await
import kotlinx.coroutines.withContext
+import org.oxycblt.auxio.util.unlikelyToBeNull
+import org.oxycblt.ktaglib.FileRef
+import org.oxycblt.ktaglib.KTagLib
+import org.oxycblt.ktaglib.Metadata
import org.oxycblt.musikr.fs.query.DeviceFile
interface MetadataExtractor {
- suspend fun extract(file: DeviceFile): AudioMetadata
+ suspend fun extract(file: DeviceFile): Metadata?
}
-class MetadataExtractorImpl
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
- private val mediaSourceFactory: MediaSource.Factory
-) : MetadataExtractor {
- 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 AudioMetadata(null, mediaMetadataRetriever)
+class MetadataExtractorImpl @Inject constructor(@ApplicationContext private val context: Context) :
+ MetadataExtractor {
+ override suspend fun extract(file: DeviceFile) =
+ withContext(Dispatchers.IO) {
+ KTagLib.open(context, FileRef(unlikelyToBeNull(file.path.name), file.uri))
}
- val trackGroup = trackGroupArray.get(0)
- if (trackGroup.length == 0) {
- return AudioMetadata(null, mediaMetadataRetriever)
- }
- val format = trackGroup.getFormat(0)
- return AudioMetadata(format, mediaMetadataRetriever)
- }
}
diff --git a/app/src/main/java/org/oxycblt/musikr/metadata/ReusableMetadataRetriever.kt b/app/src/main/java/org/oxycblt/musikr/metadata/ReusableMetadataRetriever.kt
deleted file mode 100644
index 27f844156..000000000
--- a/app/src/main/java/org/oxycblt/musikr/metadata/ReusableMetadataRetriever.kt
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * ReusableMetadataRetriever.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.metadata
-
-import android.os.Handler
-import android.os.HandlerThread
-import android.os.Message
-import androidx.media3.common.C
-import androidx.media3.common.MediaItem
-import androidx.media3.common.Timeline
-import androidx.media3.common.util.Clock
-import androidx.media3.common.util.HandlerWrapper
-import androidx.media3.exoplayer.LoadingInfo
-import androidx.media3.exoplayer.analytics.PlayerId
-import androidx.media3.exoplayer.source.MediaPeriod
-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.SettableFuture
-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
-private const val MESSAGE_CHECK_FAILURE = 2
-private const val MESSAGE_RELEASE = 3
-private const val CHECK_INTERVAL_MS = 100
-
-// TODO: Rewrite and re-integrate
-
-interface MetadataRetrieverExt {
- fun retrieveMetadata(mediaItem: MediaItem): Future
-
- fun retrieve()
-
- interface Factory {
- fun create(): MetadataRetrieverExt
- }
-}
-
-class ReusableMetadataRetrieverImpl
-@Inject
-constructor(private val mediaSourceFactory: MediaSource.Factory) :
- MetadataRetrieverExt, Handler.Callback {
- private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}")
- private val mediaSourceHandler: HandlerWrapper
- private var job: MetadataJob? = null
-
- private data class JobParams(
- val mediaItem: MediaItem,
- val future: SettableFuture
- )
-
- private class JobData(
- val params: JobParams,
- val mediaSource: MediaSource,
- var mediaPeriod: MediaPeriod?,
- )
-
- private class MetadataJob(val data: JobData, val mediaSourceCaller: MediaSourceCaller)
-
- init {
- mediaSourceThread.start()
- mediaSourceHandler = Clock.DEFAULT.createHandler(mediaSourceThread.looper, this)
- }
-
- override fun retrieveMetadata(mediaItem: MediaItem): Future {
- val job = job
- check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
- val future = SettableFuture.create()
- mediaSourceHandler
- .obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future))
- .sendToTarget()
- return future
- }
-
- override fun retrieve() {
- mediaSourceHandler.removeCallbacksAndMessages(null)
- mediaSourceThread.quit()
- }
-
- override fun handleMessage(msg: Message): Boolean {
- when (msg.what) {
- MESSAGE_PREPARE -> {
- val params = msg.obj as JobParams
-
- val mediaSource = mediaSourceFactory.createMediaSource(params.mediaItem)
- val data = JobData(params, mediaSource, null)
- val mediaSourceCaller = MediaSourceCaller(data)
- mediaSource.prepareSource(
- mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET)
- job = MetadataJob(data, mediaSourceCaller)
-
- mediaSourceHandler.sendEmptyMessageDelayed(
- MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS)
-
- return true
- }
- MESSAGE_CONTINUE_LOADING -> {
- val job = job ?: return true
- checkNotNull(job.data.mediaPeriod)
- .continueLoading(LoadingInfo.Builder().setPlaybackPositionUs(0).build())
- return true
- }
- MESSAGE_CHECK_FAILURE -> {
- val job = job ?: return true
- val mediaPeriod = job.data.mediaPeriod
- val mediaSource = job.data.mediaSource
- val mediaSourceCaller = job.mediaSourceCaller
- try {
- if (mediaPeriod == null) {
- mediaSource.maybeThrowSourceInfoRefreshError()
- } else {
- mediaPeriod.maybeThrowPrepareError()
- }
- } catch (e: Exception) {
- Timber.e("Failed to extract MediaSource")
- Timber.e(e.stackTraceToString())
- mediaPeriod?.let(mediaSource::releasePeriod)
- mediaSource.releaseSource(mediaSourceCaller)
- job.data.params.future.setException(e)
- }
- return true
- }
- MESSAGE_RELEASE -> {
- val job = job ?: return true
- val mediaPeriod = job.data.mediaPeriod
- val mediaSource = job.data.mediaSource
- val mediaSourceCaller = job.mediaSourceCaller
- mediaPeriod?.let { mediaSource.releasePeriod(it) }
- mediaSource.releaseSource(mediaSourceCaller)
- this.job = null
- return true
- }
- else -> return false
- }
- }
-
- private inner class MediaSourceCaller(private val data: JobData) :
- MediaSource.MediaSourceCaller {
-
- private val mediaPeriodCallback: MediaPeriodCallback =
- MediaPeriodCallback(data.params.future)
- private val allocator: Allocator =
- DefaultAllocator(
- /* trimOnReset= */ true,
- /* individualAllocationSize= */ C.DEFAULT_BUFFER_SEGMENT_SIZE)
-
- private var mediaPeriodCreated = false
-
- override fun onSourceInfoRefreshed(source: MediaSource, timeline: Timeline) {
- if (mediaPeriodCreated) {
- // Ignore dynamic updates.
- return
- }
- mediaPeriodCreated = true
- val mediaPeriod =
- source.createPeriod(
- MediaSource.MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 0)),
- allocator,
- /* startPositionUs= */ 0)
- data.mediaPeriod = mediaPeriod
- mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0)
- }
-
- private inner class MediaPeriodCallback(
- private val future: SettableFuture
- ) : MediaPeriod.Callback {
- override fun onPrepared(mediaPeriod: MediaPeriod) {
- future.set(mediaPeriod.getTrackGroups())
- mediaSourceHandler.sendEmptyMessage(MESSAGE_RELEASE)
- }
-
- @Override
- override fun onContinueLoadingRequested(source: MediaPeriod) {
- mediaSourceHandler.sendEmptyMessage(MESSAGE_CONTINUE_LOADING)
- }
- }
- }
-}
diff --git a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
index 8334df78d..918790a45 100644
--- a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
+++ b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
@@ -26,15 +26,16 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.cover.CoverParser
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser
+import timber.log.Timber as L
interface ExtractStep {
fun extract(storage: Storage, nodes: Flow): Flow
@@ -42,11 +43,8 @@ interface ExtractStep {
class ExtractStepImpl
@Inject
-constructor(
- private val metadataExtractor: MetadataExtractor,
- private val tagParser: TagParser,
- private val coverParser: CoverParser
-) : ExtractStep {
+constructor(private val metadataExtractor: MetadataExtractor, private val tagParser: TagParser) :
+ ExtractStep {
override fun extract(storage: Storage, nodes: Flow): Flow {
val cacheResults =
nodes
@@ -63,15 +61,16 @@ constructor(
ExtractedMusic.Song(it.file, song.parsedTags, song.cover)
}
}
- val split = uncachedSongs.distribute(8)
+ val split = uncachedSongs.distribute(16)
val extractedSongs =
Array(split.hot.size) { i ->
split.hot[i]
- .map { node ->
- val metadata = metadataExtractor.extract(node.file)
+ .mapNotNull { node ->
+ val metadata =
+ metadataExtractor.extract(node.file) ?: return@mapNotNull null
+ L.d("Extracted tags for ${metadata.id3v2}")
val tags = tagParser.parse(node.file, metadata)
- val coverData = coverParser.extract(metadata)
- val cover = coverData?.let { storage.storedCovers.write(it) }
+ val cover = metadata.cover?.let { storage.storedCovers.write(it) }
ExtractedMusic.Song(node.file, tags, cover)
}
.flowOn(Dispatchers.IO)
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTagFields.kt b/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTagFields.kt
index 3110445cf..76f72fb16 100644
--- a/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTagFields.kt
+++ b/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTagFields.kt
@@ -20,37 +20,38 @@ package org.oxycblt.musikr.tag.parse
import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.util.nonZeroOrNull
+import org.oxycblt.ktaglib.Metadata
import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.util.parseId3v2PositionField
-import org.oxycblt.musikr.tag.util.parseVorbisPositionField
+import org.oxycblt.musikr.tag.util.parseXiphPositionField
// Song
-fun ExoPlayerTags.musicBrainzId() =
- (vorbis["musicbrainz_releasetrackid"]
- ?: vorbis["musicbrainz release track id"]
+fun Metadata.musicBrainzId() =
+ (xiph["musicbrainz_releasetrackid"]
+ ?: xiph["musicbrainz release track id"]
?: id3v2["TXXX:musicbrainz release track id"]
?: id3v2["TXXX:musicbrainz_releasetrackid"])
?.first()
-fun ExoPlayerTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first()
+fun Metadata.name() = (xiph["title"] ?: id3v2["TIT2"])?.first()
-fun ExoPlayerTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first()
+fun Metadata.sortName() = (xiph["titlesort"] ?: id3v2["TSOT"])?.first()
// Track.
-fun ExoPlayerTags.track() =
- (parseVorbisPositionField(
- vorbis["tracknumber"]?.first(),
- (vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first())
+fun Metadata.track() =
+ (parseXiphPositionField(
+ xiph["tracknumber"]?.first(),
+ (xiph["totaltracks"] ?: xiph["tracktotal"] ?: xiph["trackc"])?.first())
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
// Disc and it's subtitle name.
-fun ExoPlayerTags.disc() =
- (parseVorbisPositionField(
- vorbis["discnumber"]?.first(),
- (vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
+fun Metadata.disc() =
+ (parseXiphPositionField(
+ xiph["discnumber"]?.first(),
+ (xiph["totaldiscs"] ?: xiph["disctotal"] ?: xiph["discc"])?.run { first() })
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
-fun ExoPlayerTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
+fun Metadata.subtitle() = (xiph["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,17 +65,17 @@ fun ExoPlayerTags.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 ExoPlayerTags.date() =
- (vorbis["originaldate"]?.run { Date.from(first()) }
- ?: vorbis["date"]?.run { Date.from(first()) }
- ?: vorbis["year"]?.run { Date.from(first()) }
+fun Metadata.date() =
+ (xiph["originaldate"]?.run { Date.from(first()) }
+ ?: xiph["date"]?.run { Date.from(first()) }
+ ?: xiph["year"]?.run { Date.from(first()) }
?:
- // Vorbis dates are less complicated, but there are still several types
+ // xiph dates are less complicated, but there are still several types
// Our hierarchy for dates is as such:
// 1. Original Date, as it solves the "Released in X, Remastered in Y" issue
// 2. Date, as it is the most common date type
- // 3. Year, as old vorbis tags tended to use this (I know this because it's the only
+ // 3. Year, as old xiph tags tended to use this (I know this because it's the only
// date tag that android supports, so it must be 15 years old or more!)
id3v2["TDOR"]?.run { Date.from(first()) }
?: id3v2["TDRC"]?.run { Date.from(first()) }
@@ -82,20 +83,20 @@ fun ExoPlayerTags.date() =
?: parseId3v23Date())
// Album
-fun ExoPlayerTags.albumMusicBrainzId() =
- (vorbis["musicbrainz_albumid"]
- ?: vorbis["musicbrainz album id"]
+fun Metadata.albumMusicBrainzId() =
+ (xiph["musicbrainz_albumid"]
+ ?: xiph["musicbrainz album id"]
?: id3v2["TXXX:musicbrainz album id"]
?: id3v2["TXXX:musicbrainz_albumid"])
?.first()
-fun ExoPlayerTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first()
+fun Metadata.albumName() = (xiph["album"] ?: id3v2["TALB"])?.first()
-fun ExoPlayerTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first()
+fun Metadata.albumSortName() = (xiph["albumsort"] ?: id3v2["TSOA"])?.first()
-fun ExoPlayerTags.releaseTypes() =
- (vorbis["releasetype"]
- ?: vorbis["musicbrainz album type"]
+fun Metadata.releaseTypes() =
+ (xiph["releasetype"]
+ ?: xiph["musicbrainz album type"]
?: id3v2["TXXX:musicbrainz album type"]
?: id3v2["TXXX:releasetype"]
?:
@@ -103,25 +104,25 @@ fun ExoPlayerTags.releaseTypes() =
id3v2["GRP1"])
// Artist
-fun ExoPlayerTags.artistMusicBrainzIds() =
- (vorbis["musicbrainz_artistid"]
- ?: vorbis["musicbrainz artist id"]
+fun Metadata.artistMusicBrainzIds() =
+ (xiph["musicbrainz_artistid"]
+ ?: xiph["musicbrainz artist id"]
?: id3v2["TXXX:musicbrainz artist id"]
?: id3v2["TXXX:musicbrainz_artistid"])
-fun ExoPlayerTags.artistNames() =
- (vorbis["artists"]
- ?: vorbis["artist"]
+fun Metadata.artistNames() =
+ (xiph["artists"]
+ ?: xiph["artist"]
?: id3v2["TXXX:artists"]
?: id3v2["TPE1"]
?: id3v2["TXXX:artist"])
-fun ExoPlayerTags.artistSortNames() =
- (vorbis["artistssort"]
- ?: vorbis["artists_sort"]
- ?: vorbis["artists sort"]
- ?: vorbis["artistsort"]
- ?: vorbis["artist sort"]
+fun Metadata.artistSortNames() =
+ (xiph["artistssort"]
+ ?: xiph["artists_sort"]
+ ?: xiph["artists sort"]
+ ?: xiph["artistsort"]
+ ?: xiph["artist sort"]
?: id3v2["TXXX:artistssort"]
?: id3v2["TXXX:artists_sort"]
?: id3v2["TXXX:artists sort"]
@@ -129,18 +130,18 @@ fun ExoPlayerTags.artistSortNames() =
?: id3v2["artistsort"]
?: id3v2["TXXX:artist sort"])
-fun ExoPlayerTags.albumArtistMusicBrainzIds() =
- (vorbis["musicbrainz_albumartistid"]
- ?: vorbis["musicbrainz album artist id"]
+fun Metadata.albumArtistMusicBrainzIds() =
+ (xiph["musicbrainz_albumartistid"]
+ ?: xiph["musicbrainz album artist id"]
?: id3v2["TXXX:musicbrainz album artist id"]
?: id3v2["TXXX:musicbrainz_albumartistid"])
-fun ExoPlayerTags.albumArtistNames() =
- (vorbis["albumartists"]
- ?: vorbis["album_artists"]
- ?: vorbis["album artists"]
- ?: vorbis["albumartist"]
- ?: vorbis["album artist"]
+fun Metadata.albumArtistNames() =
+ (xiph["albumartists"]
+ ?: xiph["album_artists"]
+ ?: xiph["album artists"]
+ ?: xiph["albumartist"]
+ ?: xiph["album artist"]
?: id3v2["TXXX:albumartists"]
?: id3v2["TXXX:album_artists"]
?: id3v2["TXXX:album artists"]
@@ -148,12 +149,12 @@ fun ExoPlayerTags.albumArtistNames() =
?: id3v2["TXXX:albumartist"]
?: id3v2["TXXX:album artist"])
-fun ExoPlayerTags.albumArtistSortNames() =
- (vorbis["albumartistssort"]
- ?: vorbis["albumartists_sort"]
- ?: vorbis["albumartists sort"]
- ?: vorbis["albumartistsort"]
- ?: vorbis["album artist sort"]
+fun Metadata.albumArtistSortNames() =
+ (xiph["albumartistssort"]
+ ?: xiph["albumartists_sort"]
+ ?: xiph["albumartists sort"]
+ ?: xiph["albumartistsort"]
+ ?: xiph["album artist sort"]
?: id3v2["TXXX:albumartistssort"]
?: id3v2["TXXX:albumartists_sort"]
?: id3v2["TXXX:albumartists sort"]
@@ -163,12 +164,12 @@ fun ExoPlayerTags.albumArtistSortNames() =
?: id3v2["TXXX:album artist sort"])
// Genre
-fun ExoPlayerTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
+fun Metadata.genreNames() = xiph["genre"] ?: id3v2["TCON"]
// Compilation Flag
-fun ExoPlayerTags.isCompilation() =
- (vorbis["compilation"]
- ?: vorbis["itunescompilation"]
+fun Metadata.isCompilation() =
+ (xiph["compilation"]
+ ?: xiph["itunescompilation"]
?: id3v2["TCMP"] // This is a non-standard itunes extension
?: id3v2["TXXX:compilation"]
?: id3v2["TXXX:itunescompilation"])
@@ -178,17 +179,17 @@ fun ExoPlayerTags.isCompilation() =
}
// ReplayGain information
-fun ExoPlayerTags.replayGainTrackAdjustment() =
- (vorbis["r128_track_gain"]?.parseR128Adjustment()
- ?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
+fun Metadata.replayGainTrackAdjustment() =
+ (xiph["r128_track_gain"]?.parseR128Adjustment()
+ ?: xiph["replaygain_track_gain"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
-fun ExoPlayerTags.replayGainAlbumAdjustment() =
- (vorbis["r128_album_gain"]?.parseR128Adjustment()
- ?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
+fun Metadata.replayGainAlbumAdjustment() =
+ (xiph["r128_album_gain"]?.parseR128Adjustment()
+ ?: xiph["replaygain_album_gain"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment())
-private fun ExoPlayerTags.parseId3v23Date(): Date? {
+private fun Metadata.parseId3v23Date(): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
// is present.
val year =
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTags.kt b/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTags.kt
deleted file mode 100644
index 235d143a1..000000000
--- a/app/src/main/java/org/oxycblt/musikr/tag/parse/ExoPlayerTags.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (c) 2023 Auxio Project
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.tag.parse
-
-import androidx.media3.common.Metadata
-import androidx.media3.extractor.metadata.id3.InternalFrame
-import androidx.media3.extractor.metadata.id3.TextInformationFrame
-import androidx.media3.extractor.metadata.vorbis.VorbisComment
-import org.oxycblt.musikr.tag.util.correctWhitespace
-
-/**
- * Processing wrapper for [Metadata] that allows organized access to text-based audio tags.
- *
- * @param metadata The [Metadata] to wrap.
- * @author Alexander Capehart (OxygenCobalt)
- */
-class ExoPlayerTags(metadata: Metadata) {
- private val _id3v2 = mutableMapOf>()
- /** The ID3v2 text identification frames found in the file. Can have more than one value. */
- val id3v2: Map>
- get() = _id3v2
-
- private val _vorbis = mutableMapOf>()
- /** The vorbis comments found in the file. Can have more than one value. */
- val vorbis: Map>
- get() = _vorbis
-
- init {
- for (i in 0 until metadata.length()) {
- when (val tag = metadata[i]) {
- is TextInformationFrame -> {
- // Map TXXX frames differently so we can specifically index by their
- // descriptions.
- val id =
- tag.description?.let { "TXXX:${it.sanitize().lowercase()}" }
- ?: tag.id.sanitize()
- val values = tag.values.map { it.sanitize() }.correctWhitespace()
- if (values.isNotEmpty()) {
- // Normally, duplicate ID3v2 frames are forbidden. But since MP4 atoms,
- // which can also have duplicates, are mapped to ID3v2 frames by ExoPlayer,
- // we must drop this invariant and gracefully treat duplicates as if they
- // are another way of specfiying multi-value tags.
- _id3v2.getOrPut(id) { mutableListOf() }.addAll(values)
- }
- }
- is InternalFrame -> {
- // Most MP4 metadata atoms map to ID3v2 text frames, except for the ---- atom,
- // which has it's own frame. Map this to TXXX, it's rough ID3v2 equivalent.
- val id = "TXXX:${tag.description.sanitize().lowercase()}"
- val value = tag.text
- if (value.isNotEmpty()) {
- _id3v2.getOrPut(id) { mutableListOf() }.add(value)
- }
- }
- is VorbisComment -> {
- // Vorbis comment keys can be in any case, make them uppercase for simplicity.
- val id = tag.key.sanitize().lowercase()
- if (id == "metadata_block_picture") {
- // Picture, we don't care about these
- continue
- }
- val value = tag.value.sanitize().correctWhitespace()
- if (value != null) {
- _vorbis.getOrPut(id) { mutableListOf() }.add(value)
- }
- }
- }
- }
- }
-
- /**
- * Copies and sanitizes a possibly invalid string outputted from ExoPlayer.
- *
- * @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with
- * the Unicode replacement byte sequence.
- */
- private fun String.sanitize() = String(encodeToByteArray())
-}
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/parse/MediaMetadataTagFields.kt b/app/src/main/java/org/oxycblt/musikr/tag/parse/MediaMetadataTagFields.kt
deleted file mode 100644
index 7751ffbc7..000000000
--- a/app/src/main/java/org/oxycblt/musikr/tag/parse/MediaMetadataTagFields.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.tag.parse
-
-import android.media.MediaMetadataRetriever
-
-fun MediaMetadataRetriever.durationMs() =
- extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt b/app/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
index bf1071c42..696a5fc4e 100644
--- a/app/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
+++ b/app/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
@@ -19,48 +19,38 @@
package org.oxycblt.musikr.tag.parse
import javax.inject.Inject
+import org.oxycblt.ktaglib.Metadata
import org.oxycblt.musikr.fs.query.DeviceFile
-import org.oxycblt.musikr.metadata.AudioMetadata
interface TagParser {
- fun parse(file: DeviceFile, metadata: AudioMetadata): ParsedTags
+ fun parse(file: DeviceFile, metadata: Metadata): 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)
+ override fun parse(file: DeviceFile, metadata: Metadata): ParsedTags {
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())
+ durationMs = metadata.properties.durationMs,
+ replayGainTrackAdjustment = metadata.replayGainTrackAdjustment(),
+ replayGainAlbumAdjustment = metadata.replayGainAlbumAdjustment(),
+ musicBrainzId = metadata.musicBrainzId(),
+ name = metadata.name() ?: file.path.name ?: throw MissingTagError("name"),
+ sortName = metadata.sortName(),
+ track = metadata.track(),
+ disc = metadata.disc(),
+ subtitle = metadata.subtitle(),
+ date = metadata.date(),
+ albumMusicBrainzId = metadata.albumMusicBrainzId(),
+ albumName = metadata.albumName(),
+ albumSortName = metadata.albumSortName(),
+ releaseTypes = metadata.releaseTypes() ?: listOf(),
+ artistMusicBrainzIds = metadata.artistMusicBrainzIds() ?: listOf(),
+ artistNames = metadata.artistNames() ?: listOf(),
+ artistSortNames = metadata.artistSortNames() ?: listOf(),
+ albumArtistMusicBrainzIds = metadata.albumArtistMusicBrainzIds() ?: listOf(),
+ albumArtistNames = metadata.albumArtistNames() ?: listOf(),
+ albumArtistSortNames = metadata.albumArtistSortNames() ?: listOf(),
+ genreNames = metadata.genreNames() ?: listOf())
}
}
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/util/Vorbis.kt b/app/src/main/java/org/oxycblt/musikr/tag/util/Vorbis.kt
index beada6676..7e8378a7a 100644
--- a/app/src/main/java/org/oxycblt/musikr/tag/util/Vorbis.kt
+++ b/app/src/main/java/org/oxycblt/musikr/tag/util/Vorbis.kt
@@ -47,7 +47,7 @@ fun String.parseId3v2PositionField() =
*
* @see transformPositionField
*/
-fun parseVorbisPositionField(pos: String?, total: String?) =
+fun parseXiphPositionField(pos: String?, total: String?) =
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
/**
diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml
index e1ea02bd1..6bc79e6d3 100644
--- a/app/src/main/res/values/settings.xml
+++ b/app/src/main/res/values/settings.xml
@@ -18,7 +18,7 @@
auxio_square_covers
auxio_include_dirs
auxio_exclude_non_music
- auxio_music_locations
+ auxio_music_locations2
auxio_separators
auxio_auto_sort_names
diff --git a/app/src/test/java/org/oxycblt/musikr/tag/parse/TextTagsTest.kt b/app/src/test/java/org/oxycblt/musikr/tag/parse/TextTagsTest.kt
deleted file mode 100644
index 2f6a75a30..000000000
--- a/app/src/test/java/org/oxycblt/musikr/tag/parse/TextTagsTest.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (c) 2023 Auxio Project
- * TextTagsTest.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.musikr.tag.parse
-
-import androidx.media3.common.Metadata
-import androidx.media3.extractor.metadata.flac.PictureFrame
-import androidx.media3.extractor.metadata.id3.ApicFrame
-import androidx.media3.extractor.metadata.id3.InternalFrame
-import androidx.media3.extractor.metadata.id3.TextInformationFrame
-import androidx.media3.extractor.metadata.vorbis.VorbisComment
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class TextTagsTest {
- @Test
- fun textTags_vorbis() {
- 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 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 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 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"), 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 {
- private val VORBIS_METADATA =
- Metadata(
- VorbisComment("TITLE", "Wheel"),
- VorbisComment("ALBUM", "Paraglow"),
- VorbisComment("ARTIST", "Parannoul"),
- VorbisComment("ARTIST", "Asian Glow"),
- VorbisComment("DATE", "2022"),
- VorbisComment("RELEASETYPE", "ep"),
- VorbisComment("METADATA_BLOCK_PICTURE", ""),
- VorbisComment("REPLAYGAIN_TRACK_GAIN", "+2 dB"),
- PictureFrame(0, "", "", 0, 0, 0, 0, byteArrayOf()))
-
- private val ID3V2_METADATA =
- Metadata(
- TextInformationFrame("TIT2", null, listOf("Wheel")),
- TextInformationFrame("TALB", null, listOf("Paraglow")),
- TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")),
- TextInformationFrame("TDRC", null, listOf("2022")),
- TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")),
- TextInformationFrame("TXXX", "replaygain_track_gain", listOf("+2 dB")),
- ApicFrame("", "", 0, byteArrayOf()))
-
- // MP4 atoms are mapped to ID3v2 text information frames by ExoPlayer, but can
- // duplicate frames and have ---- mapped to InternalFrame.
- private val MP4_METADATA =
- Metadata(
- TextInformationFrame("TIT2", null, listOf("Wheel")),
- TextInformationFrame("TALB", null, listOf("Paraglow")),
- TextInformationFrame("TPE1", null, listOf("Parannoul")),
- TextInformationFrame("TPE1", null, listOf("Asian Glow")),
- TextInformationFrame("TDRC", null, listOf("2022")),
- TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")),
- InternalFrame("com.apple.iTunes", "replaygain_track_gain", "+2 dB"),
- ApicFrame("", "", 0, byteArrayOf()))
- }
-}