musikr: start using ktaglib
This commit is contained in:
parent
2f98d67855
commit
65151e006f
13 changed files with 114 additions and 713 deletions
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.musikr.metadata
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import androidx.media3.common.Format
|
||||
|
||||
data class AudioMetadata(
|
||||
val exoPlayerFormat: Format?,
|
||||
val mediaMetadataRetriever: MediaMetadataRetriever
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
val trackGroup = trackGroupArray.get(0)
|
||||
if (trackGroup.length == 0) {
|
||||
return AudioMetadata(null, mediaMetadataRetriever)
|
||||
}
|
||||
val format = trackGroup.getFormat(0)
|
||||
return AudioMetadata(format, 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<TrackGroupArray>
|
||||
|
||||
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<TrackGroupArray>
|
||||
)
|
||||
|
||||
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<TrackGroupArray> {
|
||||
val job = job
|
||||
check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
|
||||
val future = SettableFuture.create<TrackGroupArray>()
|
||||
mediaSourceHandler
|
||||
.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future))
|
||||
.sendToTarget()
|
||||
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<TrackGroupArray>
|
||||
) : 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ExploreNode>): Flow<ExtractedMusic>
|
||||
|
@ -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<ExploreNode>): Flow<ExtractedMusic> {
|
||||
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)
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String, MutableList<String>>()
|
||||
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
|
||||
val id3v2: Map<String, List<String>>
|
||||
get() = _id3v2
|
||||
|
||||
private val _vorbis = mutableMapOf<String, MutableList<String>>()
|
||||
/** The vorbis comments found in the file. Can have more than one value. */
|
||||
val vorbis: Map<String, List<String>>
|
||||
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())
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.musikr.tag.parse
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
|
||||
fun MediaMetadataRetriever.durationMs() =
|
||||
extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
|
||||
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
||||
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
||||
<string name="set_key_music_locations" translatable="false">auxio_music_locations</string>
|
||||
<string name="set_key_music_locations" translatable="false">auxio_music_locations2</string>
|
||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||
<string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string>
|
||||
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue