musikr: restructure loader into pipeline
This commit is contained in:
parent
7582c8c9cf
commit
7f7ee94f45
52 changed files with 753 additions and 508 deletions
|
@ -34,9 +34,9 @@ import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
|||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.metadata.AudioProperties
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.musikr.metadata.AudioProperties
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.replaygain.formatDb
|
||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
||||
|
|
|
@ -65,8 +65,8 @@ import org.oxycblt.auxio.music.MusicViewModel
|
|||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.PlaylistDecision
|
||||
import org.oxycblt.auxio.music.PlaylistMessage
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import org.oxycblt.auxio.musikr.IndexingProgress
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import org.oxycblt.auxio.playback.PlaybackDecision
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.collect
|
||||
|
|
|
@ -43,8 +43,8 @@ import kotlinx.coroutines.withContext
|
|||
import okio.FileSystem
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.image.stack.CoverRetriever
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
|
||||
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
||||
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
|
||||
|
|
|
@ -20,9 +20,9 @@ package org.oxycblt.auxio.image.stack
|
|||
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.image.stack.extractor.CoverExtractor
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.cover.CoverCache
|
||||
import org.oxycblt.auxio.image.stack.extractor.CoverExtractor
|
||||
import timber.log.Timber
|
||||
|
||||
interface CoverRetriever {
|
||||
|
|
|
@ -35,6 +35,7 @@ interface CoverSource {
|
|||
class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) :
|
||||
CoverExtractor {
|
||||
override suspend fun extract(cover: Cover.Single): ByteArray? {
|
||||
return null
|
||||
for (coverSource in coverSources.sources) {
|
||||
val stream = coverSource.extract(cover.uri)
|
||||
if (stream != null) {
|
||||
|
|
|
@ -27,14 +27,14 @@ import java.util.UUID
|
|||
import kotlin.math.max
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
import org.oxycblt.auxio.musikr.tag.Disc
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.concatLocalized
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
|
|
@ -25,12 +25,12 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
||||
import org.oxycblt.auxio.musikr.Indexer
|
||||
import org.oxycblt.auxio.musikr.IndexingProgress
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ import androidx.core.content.edit
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.musikr.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicLocationsBinding
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.musikr.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import timber.log.Timber as L
|
||||
|
|
|
@ -20,17 +20,16 @@ package org.oxycblt.auxio.musikr
|
|||
|
||||
import android.net.Uri
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.oxycblt.auxio.musikr.explore.Explorer
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.model.Modeler
|
||||
import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.pipeline.EvaluateStep
|
||||
import org.oxycblt.auxio.musikr.pipeline.ExploreStep
|
||||
import org.oxycblt.auxio.musikr.pipeline.ExtractStep
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
|
||||
interface Indexer {
|
||||
suspend fun run(
|
||||
|
@ -53,22 +52,19 @@ sealed interface IndexingProgress {
|
|||
|
||||
class IndexerImpl
|
||||
@Inject
|
||||
constructor(private val explorer: Explorer, private val modeler: Modeler) : Indexer {
|
||||
constructor(
|
||||
private val exploreStep: ExploreStep,
|
||||
private val extractStep: ExtractStep,
|
||||
private val evaluateStep: EvaluateStep
|
||||
) : Indexer {
|
||||
override suspend fun run(
|
||||
uris: List<Uri>,
|
||||
interpretation: Interpretation,
|
||||
onProgress: suspend (IndexingProgress) -> Unit
|
||||
) = coroutineScope {
|
||||
val files = explorer.explore(uris, onProgress)
|
||||
val audioFiles =
|
||||
files.audios
|
||||
.cap(
|
||||
start = { onProgress(IndexingProgress.Songs(0, 0)) },
|
||||
end = { onProgress(IndexingProgress.Indeterminate) })
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED)
|
||||
modeler.model(audioFiles, playlistFiles, interpretation)
|
||||
val explored = exploreStep.explore(uris).buffer(Channel.UNLIMITED)
|
||||
val extracted = extractStep.extract(explored).buffer(Channel.UNLIMITED)
|
||||
evaluateStep.evaluate(interpretation, extracted)
|
||||
}
|
||||
|
||||
private fun <T> Flow<T>.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow<T> =
|
||||
|
|
|
@ -24,7 +24,14 @@ import org.oxycblt.auxio.music.Song
|
|||
sealed interface Cover {
|
||||
val key: String
|
||||
|
||||
data class Single(override val key: String) : Cover
|
||||
data class Single(val song: Song) : Cover {
|
||||
override val key: String
|
||||
get() = "${song.uid}@${song.lastModified}"
|
||||
|
||||
val uid = song.uid
|
||||
val uri = song.uri
|
||||
val lastModified = song.lastModified
|
||||
}
|
||||
|
||||
class Multi(val all: List<Single>) : Cover {
|
||||
override val key = "multi@${all.hashCode()}"
|
||||
|
@ -35,7 +42,7 @@ sealed interface Cover {
|
|||
|
||||
fun nil() = Multi(listOf())
|
||||
|
||||
fun single(key: String) = Single(key)
|
||||
fun single(song: Song) = Single(song)
|
||||
|
||||
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
|
||||
|
||||
|
|
|
@ -44,8 +44,7 @@ constructor(
|
|||
val id = coverIdentifier.identify(data)
|
||||
coverFiles.write(id, data)
|
||||
storedCoversDao.setStoredCover(
|
||||
StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id)
|
||||
)
|
||||
StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id))
|
||||
return coverFiles.read(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Explorer.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.explore
|
||||
|
||||
import android.net.Uri
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
import org.oxycblt.auxio.musikr.IndexingProgress
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.tag.cache.CacheResult
|
||||
import org.oxycblt.auxio.musikr.tag.cache.TagCache
|
||||
import org.oxycblt.auxio.musikr.tag.extractor.TagExtractor
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFiles
|
||||
import org.oxycblt.auxio.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
||||
import timber.log.Timber
|
||||
|
||||
interface Explorer {
|
||||
fun explore(uris: List<Uri>, onProgress: suspend (IndexingProgress.Songs) -> Unit): Files
|
||||
}
|
||||
|
||||
data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
|
||||
|
||||
class ExplorerImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val deviceFiles: DeviceFiles,
|
||||
private val tagCache: TagCache,
|
||||
private val tagExtractor: TagExtractor,
|
||||
private val storedPlaylists: StoredPlaylists
|
||||
) : Explorer {
|
||||
override fun explore(
|
||||
uris: List<Uri>,
|
||||
onProgress: suspend (IndexingProgress.Songs) -> Unit
|
||||
): Files {
|
||||
var loaded = 0
|
||||
var explored = 0
|
||||
val deviceFiles =
|
||||
deviceFiles
|
||||
.explore(uris.asFlow())
|
||||
.onEach {
|
||||
Timber.d("File explored: $it")
|
||||
explored++
|
||||
onProgress(IndexingProgress.Songs(loaded, explored))
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
val cacheResults =
|
||||
tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED)
|
||||
val audioFiles =
|
||||
cacheResults
|
||||
.handleMisses { misses ->
|
||||
val extracted =
|
||||
misses
|
||||
.stretch(8) { tagExtractor.extract(it).flowOn(Dispatchers.IO) }
|
||||
.buffer(Channel.UNLIMITED)
|
||||
val written =
|
||||
tagCache.write(extracted).flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED)
|
||||
written
|
||||
}
|
||||
.onEach {
|
||||
loaded++
|
||||
onProgress(IndexingProgress.Songs(loaded, explored))
|
||||
Timber.d("File extracted: $it")
|
||||
}
|
||||
val playlistFiles = storedPlaylists.read()
|
||||
return Files(audioFiles, playlistFiles)
|
||||
}
|
||||
|
||||
/** Temporarily split a flow into 8 parallel threads and then */
|
||||
private fun <T, R> Flow<T>.stretch(n: Int, creator: (Flow<T>) -> Flow<R>): Flow<R> {
|
||||
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
|
||||
val divert: Flow<R> = flow {
|
||||
withIndex().collect {
|
||||
val index = it.index % n
|
||||
posChannels[index].send(it.value)
|
||||
}
|
||||
for (channel in posChannels) {
|
||||
channel.close()
|
||||
}
|
||||
}
|
||||
val handle =
|
||||
posChannels.map { creator(it.receiveAsFlow()).buffer(Channel.UNLIMITED) }.asFlow()
|
||||
return merge(divert, handle.flattenMerge())
|
||||
}
|
||||
|
||||
private fun Flow<CacheResult>.handleMisses(
|
||||
uncached: (Flow<DeviceFile>) -> Flow<AudioFile>
|
||||
): Flow<AudioFile> {
|
||||
val uncachedChannel = Channel<DeviceFile>()
|
||||
val cachedChannel = Channel<AudioFile>()
|
||||
val divert: Flow<AudioFile> = flow {
|
||||
collect {
|
||||
when (it) {
|
||||
is CacheResult.Hit -> cachedChannel.send(it.audioFile)
|
||||
is CacheResult.Miss -> uncachedChannel.send(it.deviceFile)
|
||||
}
|
||||
}
|
||||
cachedChannel.close()
|
||||
uncachedChannel.close()
|
||||
}
|
||||
val uncached = uncached(uncachedChannel.receiveAsFlow())
|
||||
val cached = cachedChannel.receiveAsFlow()
|
||||
return merge(divert, uncached, cached)
|
||||
}
|
||||
}
|
|
@ -31,12 +31,21 @@ import kotlinx.coroutines.flow.emitAll
|
|||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||
import timber.log.Timber
|
||||
|
||||
interface DeviceFiles {
|
||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
||||
}
|
||||
|
||||
data class DeviceFile(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val path: Path,
|
||||
val size: Long,
|
||||
val lastModified: Long
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DeviceFilesImpl
|
||||
@Inject
|
||||
|
@ -64,8 +73,7 @@ constructor(
|
|||
): Flow<DeviceFile> = flow {
|
||||
contentResolver.useQuery(
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
||||
PROJECTION
|
||||
) { cursor ->
|
||||
PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val displayNameIndex =
|
||||
|
@ -97,8 +105,7 @@ constructor(
|
|||
mimeType,
|
||||
newPath,
|
||||
size,
|
||||
lastModified)
|
||||
)
|
||||
lastModified))
|
||||
}
|
||||
}
|
||||
emitAll(recursive.asFlow().flattenMerge())
|
||||
|
|
|
@ -27,6 +27,9 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactoryImpl
|
||||
import org.oxycblt.auxio.musikr.fs.path.MediaStorePathInterpreter
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
|
||||
@Module
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.fs
|
||||
package org.oxycblt.auxio.musikr.fs.path
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
|
@ -25,6 +25,12 @@ import android.provider.DocumentsContract
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.musikr.fs.Components
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.fs.Volume
|
||||
import org.oxycblt.auxio.musikr.fs.VolumeManager
|
||||
import org.oxycblt.auxio.musikr.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.musikr.fs.useQuery
|
||||
|
||||
/**
|
||||
* A factory for parsing the reverse-engineered format of the URIs obtained from document picker.
|
|
@ -16,11 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.fs
|
||||
package org.oxycblt.auxio.musikr.fs.path
|
||||
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import org.oxycblt.auxio.musikr.fs.Components
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.fs.VolumeManager
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
|
@ -1,12 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* AudioMetadata.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.metadata
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.Metadata
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
|
||||
data class AudioMetadata(
|
||||
val file: DeviceFile,
|
||||
val exoPlayerFormat: Format,
|
||||
val exoPlayerFormat: Format?,
|
||||
val mediaMetadataRetriever: MediaMetadataRetriever
|
||||
)
|
||||
|
|
|
@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
|
|||
return AudioProperties(
|
||||
bitrate,
|
||||
sampleRate,
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)
|
||||
)
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* MetadataExtractor.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.metadata
|
||||
|
||||
import android.content.Context
|
||||
|
@ -6,48 +24,38 @@ import androidx.media3.common.MediaItem
|
|||
import androidx.media3.exoplayer.MetadataRetriever
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import javax.inject.Inject
|
||||
|
||||
interface MetadataExtractor {
|
||||
fun extract(files: Flow<DeviceFile>): Flow<AudioMetadata>
|
||||
suspend fun extract(file: DeviceFile): AudioMetadata
|
||||
}
|
||||
|
||||
class MetadataExtractorImpl @Inject constructor(
|
||||
class MetadataExtractorImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val mediaSourceFactory: MediaSource.Factory
|
||||
) : MetadataExtractor {
|
||||
override fun extract(files: Flow<DeviceFile>) = files.mapNotNull {
|
||||
val exoPlayerMetadataFuture = MetadataRetriever.retrieveMetadata(
|
||||
mediaSourceFactory,
|
||||
MediaItem.fromUri(it.uri)
|
||||
)
|
||||
val mediaMetadataRetriever = MediaMetadataRetriever().apply {
|
||||
withContext(Dispatchers.IO) {
|
||||
setDataSource(context, it.uri)
|
||||
override suspend fun extract(file: DeviceFile): AudioMetadata {
|
||||
val exoPlayerMetadataFuture =
|
||||
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(file.uri))
|
||||
val mediaMetadataRetriever =
|
||||
MediaMetadataRetriever().apply {
|
||||
withContext(Dispatchers.IO) { setDataSource(context, file.uri) }
|
||||
}
|
||||
}
|
||||
val trackGroupArray = exoPlayerMetadataFuture.await()
|
||||
if (trackGroupArray.isEmpty) {
|
||||
return@mapNotNull null
|
||||
return AudioMetadata(null, mediaMetadataRetriever)
|
||||
}
|
||||
val trackGroup = trackGroupArray.get(0)
|
||||
if (trackGroup.length == 0) {
|
||||
return@mapNotNull null
|
||||
return AudioMetadata(null, mediaMetadataRetriever)
|
||||
}
|
||||
val format = trackGroup.getFormat(0)
|
||||
AudioMetadata(
|
||||
it,
|
||||
format,
|
||||
mediaMetadataRetriever
|
||||
)
|
||||
return AudioMetadata(format, mediaMetadataRetriever)
|
||||
}
|
||||
}
|
|
@ -29,6 +29,5 @@ interface MetadataModule {
|
|||
@Binds
|
||||
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||
|
||||
@Binds
|
||||
fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor
|
||||
@Binds fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor
|
||||
}
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ReusableMetadataRetriever.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.metadata
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Message
|
||||
|
@ -16,11 +33,10 @@ import androidx.media3.exoplayer.source.MediaSource
|
|||
import androidx.media3.exoplayer.source.TrackGroupArray
|
||||
import androidx.media3.exoplayer.upstream.Allocator
|
||||
import androidx.media3.exoplayer.upstream.DefaultAllocator
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
private const val MESSAGE_PREPARE = 0
|
||||
private const val MESSAGE_CONTINUE_LOADING = 1
|
||||
|
@ -32,6 +48,7 @@ private const val CHECK_INTERVAL_MS = 100
|
|||
|
||||
interface MetadataRetrieverExt {
|
||||
fun retrieveMetadata(mediaItem: MediaItem): Future<TrackGroupArray>
|
||||
|
||||
fun retrieve()
|
||||
|
||||
interface Factory {
|
||||
|
@ -39,13 +56,18 @@ interface MetadataRetrieverExt {
|
|||
}
|
||||
}
|
||||
|
||||
class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) :
|
||||
class ReusableMetadataRetrieverImpl
|
||||
@Inject
|
||||
constructor(private val mediaSourceFactory: MediaSource.Factory) :
|
||||
MetadataRetrieverExt, Handler.Callback {
|
||||
private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}")
|
||||
private val mediaSourceHandler: HandlerWrapper
|
||||
private var job: MetadataJob? = null
|
||||
|
||||
private data class JobParams(val mediaItem: MediaItem, val future: SettableFuture<TrackGroupArray>)
|
||||
private data class JobParams(
|
||||
val mediaItem: MediaItem,
|
||||
val future: SettableFuture<TrackGroupArray>
|
||||
)
|
||||
|
||||
private class JobData(
|
||||
val params: JobParams,
|
||||
|
@ -64,7 +86,9 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
|||
val job = job
|
||||
check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
|
||||
val future = SettableFuture.create<TrackGroupArray>()
|
||||
mediaSourceHandler.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future)).sendToTarget()
|
||||
mediaSourceHandler
|
||||
.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future))
|
||||
.sendToTarget()
|
||||
return future
|
||||
}
|
||||
|
||||
|
@ -78,8 +102,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
|||
MESSAGE_PREPARE -> {
|
||||
val params = msg.obj as JobParams
|
||||
|
||||
val mediaSource =
|
||||
mediaSourceFactory.createMediaSource(params.mediaItem)
|
||||
val mediaSource = mediaSourceFactory.createMediaSource(params.mediaItem)
|
||||
val data = JobData(params, mediaSource, null)
|
||||
val mediaSourceCaller = MediaSourceCaller(data)
|
||||
mediaSource.prepareSource(
|
||||
|
@ -87,8 +110,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
|||
job = MetadataJob(data, mediaSourceCaller)
|
||||
|
||||
mediaSourceHandler.sendEmptyMessageDelayed(
|
||||
MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS
|
||||
)
|
||||
MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS)
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ package org.oxycblt.auxio.musikr.model.graph
|
|||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||
|
||||
class PlaylistLinker {
|
||||
fun register(
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
package org.oxycblt.auxio.musikr.model.impl
|
||||
|
||||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -27,6 +26,7 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedSong
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
|
@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.update
|
|||
class SongImpl(linkedSong: LinkedSong) : Song {
|
||||
private val preSong = linkedSong.preSong
|
||||
|
||||
override val uid = preSong.uid
|
||||
override val uid = preSong.computeUid()
|
||||
override val name = preSong.name
|
||||
override val track = preSong.track
|
||||
override val disc = preSong.disc
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
package org.oxycblt.auxio.musikr.model.impl
|
||||
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedPlaylist
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Modeler.kt is part of Auxio.
|
||||
* EvaluateStep.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
package org.oxycblt.auxio.musikr.pipeline
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -24,12 +24,11 @@ import kotlinx.coroutines.channels.Channel
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.auxio.musikr.model.graph.AlbumLinker
|
||||
import org.oxycblt.auxio.musikr.model.graph.ArtistLinker
|
||||
import org.oxycblt.auxio.musikr.model.graph.GenreLinker
|
||||
|
@ -44,25 +43,28 @@ import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
|||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.TagInterpreter
|
||||
import timber.log.Timber as L
|
||||
import timber.log.Timber
|
||||
|
||||
interface Modeler {
|
||||
suspend fun model(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
interface EvaluateStep {
|
||||
suspend fun evaluate(
|
||||
interpretation: Interpretation,
|
||||
extractedMusic: Flow<ExtractedMusic>
|
||||
): MutableLibrary
|
||||
}
|
||||
|
||||
class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter) : Modeler {
|
||||
override suspend fun model(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
class EvaluateStepImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val tagInterpreter: TagInterpreter,
|
||||
) : EvaluateStep {
|
||||
override suspend fun evaluate(
|
||||
interpretation: Interpretation,
|
||||
extractedMusic: Flow<ExtractedMusic>
|
||||
): MutableLibrary {
|
||||
val preSongs =
|
||||
tagInterpreter
|
||||
.interpret(audioFiles, interpretation)
|
||||
extractedMusic
|
||||
.filterIsInstance<ExtractedMusic.Song>()
|
||||
.map { tagInterpreter.interpret(it.file, it.tags, interpretation) }
|
||||
.flowOn(Dispatchers.Main)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
|
||||
|
@ -91,12 +93,12 @@ class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter
|
|||
val uidMap = mutableMapOf<Music.UID, SongImpl>()
|
||||
val songs =
|
||||
albumLinkedSongs.mapNotNull {
|
||||
val uid = it.preSong.uid
|
||||
val uid = it.preSong.computeUid()
|
||||
val other = uidMap[uid]
|
||||
if (other == null) {
|
||||
SongImpl(it)
|
||||
} else {
|
||||
L.d("Song @ $uid already exists at ${other.path}, ignoring")
|
||||
Timber.d("Song @ $uid already exists at ${other.path}, ignoring")
|
||||
null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExploreStep.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.pipeline
|
||||
|
||||
import android.net.Uri
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFiles
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
|
||||
interface ExploreStep {
|
||||
fun explore(uris: List<Uri>): Flow<ExploreNode>
|
||||
}
|
||||
|
||||
class ExploreStepImpl @Inject constructor(private val deviceFiles: DeviceFiles) : ExploreStep {
|
||||
override fun explore(uris: List<Uri>) =
|
||||
deviceFiles
|
||||
.explore(uris.asFlow())
|
||||
.mapNotNull {
|
||||
when {
|
||||
it.mimeType == M3U.MIME_TYPE -> null
|
||||
it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
sealed interface ExploreNode {
|
||||
data class Audio(val file: DeviceFile) : ExploreNode
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExtractStep.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.pipeline
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.metadata.MetadataExtractor
|
||||
import org.oxycblt.auxio.musikr.tag.cache.TagCache
|
||||
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||
import org.oxycblt.auxio.musikr.tag.parse.TagParser
|
||||
|
||||
interface ExtractStep {
|
||||
fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
|
||||
}
|
||||
|
||||
class ExtractStepImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val tagCache: TagCache,
|
||||
private val metadataExtractor: MetadataExtractor,
|
||||
private val tagParser: TagParser
|
||||
) : ExtractStep {
|
||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||
val cacheResults =
|
||||
nodes
|
||||
.filterIsInstance<ExploreNode.Audio>()
|
||||
.map {
|
||||
val tags = tagCache.read(it.file)
|
||||
MaybeCachedSong(it.file, tags)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
val (cachedSongs, uncachedSongs) =
|
||||
cacheResults.mapPartition {
|
||||
it.tags?.let { tags -> ExtractedMusic.Song(it.file, tags) }
|
||||
}
|
||||
val split = uncachedSongs.distribute(8)
|
||||
val extractedSongs =
|
||||
Array(split.hot.size) { i ->
|
||||
split.hot[i]
|
||||
.map {
|
||||
val metadata = metadataExtractor.extract(it.file)
|
||||
val tags = tagParser.parse(it.file, metadata)
|
||||
ExtractedMusic.Song(it.file, tags)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
return merge<ExtractedMusic>(
|
||||
cachedSongs,
|
||||
split.cold,
|
||||
*extractedSongs,
|
||||
)
|
||||
}
|
||||
|
||||
data class MaybeCachedSong(val file: DeviceFile, val tags: ParsedTags?)
|
||||
}
|
||||
|
||||
sealed interface ExtractedMusic {
|
||||
data class Song(val file: DeviceFile, val tags: ParsedTags) : ExtractedMusic
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* FlowUtil.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.pipeline
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
|
||||
data class HotCold<H, C>(val hot: H, val cold: Flow<C>)
|
||||
|
||||
inline fun <T, R> Flow<T>.mapPartition(crossinline predicate: (T) -> R?): HotCold<Flow<R>, T> {
|
||||
val passChannel = Channel<R>(Channel.UNLIMITED)
|
||||
val passFlow = passChannel.consumeAsFlow()
|
||||
val failFlow = flow {
|
||||
collect {
|
||||
val result = predicate(it)
|
||||
if (result != null) {
|
||||
passChannel.send(result)
|
||||
} else {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return HotCold(passFlow, failFlow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Equally "distributes" the values of some flow across n new flows.
|
||||
*
|
||||
* Note that this function requires the "manager" flow to be consumed alongside the split flows in
|
||||
* order to function. Without this, all of the newly split flows will simply block.
|
||||
*/
|
||||
fun <T> Flow<T>.distribute(n: Int): HotCold<Array<Flow<T>>, Nothing> {
|
||||
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
|
||||
val managerFlow =
|
||||
flow<Nothing> {
|
||||
withIndex().collect {
|
||||
val index = it.index % n
|
||||
posChannels[index].send(it.value)
|
||||
}
|
||||
for (channel in posChannels) {
|
||||
channel.close()
|
||||
}
|
||||
}
|
||||
val hotFlows = posChannels.map { it.receiveAsFlow() }.toTypedArray()
|
||||
return HotCold(hotFlows, managerFlow)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* ModelModule.kt is part of Auxio.
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* PipelineModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
package org.oxycblt.auxio.musikr.pipeline
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
@ -26,5 +26,9 @@ import dagger.hilt.components.SingletonComponent
|
|||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface InterpretModule {
|
||||
@Binds fun interpreter(interpreter: ModelerImpl): Modeler
|
||||
@Binds fun exploreStep(step: ExploreStepImpl): ExploreStep
|
||||
|
||||
@Binds fun extractStep(step: ExtractStepImpl): ExtractStep
|
||||
|
||||
@Binds fun evaluateStep(step: EvaluateStepImpl): EvaluateStep
|
||||
}
|
|
@ -23,11 +23,11 @@ import android.net.Uri
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import org.oxycblt.auxio.musikr.fs.Components
|
||||
import org.oxycblt.auxio.musikr.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
@ -39,8 +39,7 @@ import timber.log.Timber as L
|
|||
*/
|
||||
interface ExternalPlaylistManager {
|
||||
/**
|
||||
* Import the playli L.d("Unable to extract bit rate field")
|
||||
st file at the given [uri].
|
||||
* Import the playli L.d("Unable to extract bit rate field") st file at the given [uri].
|
||||
*
|
||||
* @param uri The [Uri] of the playlist file to import.
|
||||
* @return An [ImportedPlaylist] containing the paths to the files listed in the playlist file,
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* PlaylistFile.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.playlist
|
||||
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* ExternalModule.kt is part of Auxio.
|
||||
* PlaylistModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -22,8 +22,6 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3UImpl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
|
|
@ -28,7 +28,6 @@ import java.io.OutputStream
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||
import org.oxycblt.auxio.musikr.fs.Components
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.fs.Volume
|
||||
|
@ -36,6 +35,7 @@ import org.oxycblt.auxio.musikr.fs.VolumeManager
|
|||
import org.oxycblt.auxio.musikr.playlist.ExportConfig
|
||||
import org.oxycblt.auxio.musikr.playlist.ImportedPlaylist
|
||||
import org.oxycblt.auxio.musikr.playlist.PossiblePaths
|
||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
@ -154,8 +154,7 @@ constructor(
|
|||
else ->
|
||||
listOf(
|
||||
InterpretedPath(Components.parseUnix(path), false),
|
||||
InterpretedPath(Components.parseWindows(path), true)
|
||||
)
|
||||
InterpretedPath(Components.parseWindows(path), true))
|
||||
}
|
||||
|
||||
private fun expandInterpretation(
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* M3UModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.playlist.m3u
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.musikr.playlist.ExternalPlaylistManager
|
||||
import org.oxycblt.auxio.musikr.playlist.ExternalPlaylistManagerImpl
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3UImpl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package org.oxycblt.auxio.musikr.tag
|
||||
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
|
||||
data class AudioFile(
|
||||
val deviceFile: DeviceFile,
|
||||
val durationMs: Long,
|
||||
val replayGainTrackAdjustment: Float? = null,
|
||||
val replayGainAlbumAdjustment: Float? = null,
|
||||
val musicBrainzId: String? = null,
|
||||
val name: String,
|
||||
val sortName: String? = null,
|
||||
val track: Int? = null,
|
||||
val disc: Int? = null,
|
||||
val subtitle: String? = null,
|
||||
val date: Date? = null,
|
||||
val albumMusicBrainzId: String? = null,
|
||||
val albumName: String? = null,
|
||||
val albumSortName: String? = null,
|
||||
val releaseTypes: List<String> = listOf(),
|
||||
val artistMusicBrainzIds: List<String> = listOf(),
|
||||
val artistNames: List<String> = listOf(),
|
||||
val artistSortNames: List<String> = listOf(),
|
||||
val albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
val albumArtistNames: List<String> = listOf(),
|
||||
val albumArtistSortNames: List<String> = listOf(),
|
||||
val genreNames: List<String> = listOf()
|
||||
)
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Interpretation.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.tag
|
||||
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
||||
|
|
|
@ -19,35 +19,19 @@
|
|||
package org.oxycblt.auxio.musikr.tag.cache
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
|
||||
sealed interface CacheResult {
|
||||
data class Hit(val audioFile: AudioFile) : CacheResult
|
||||
|
||||
data class Miss(val deviceFile: DeviceFile) : CacheResult
|
||||
}
|
||||
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||
|
||||
interface TagCache {
|
||||
fun read(files: Flow<DeviceFile>): Flow<CacheResult>
|
||||
suspend fun read(file: DeviceFile): ParsedTags?
|
||||
|
||||
fun write(rawSongs: Flow<AudioFile>): Flow<AudioFile>
|
||||
suspend fun write(file: DeviceFile, tags: ParsedTags)
|
||||
}
|
||||
|
||||
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
||||
override fun read(files: Flow<DeviceFile>) =
|
||||
files.map { file ->
|
||||
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
|
||||
if (tags != null) {
|
||||
CacheResult.Hit(tags.toAudioFile(file))
|
||||
} else {
|
||||
CacheResult.Miss(file)
|
||||
}
|
||||
}
|
||||
override suspend fun read(file: DeviceFile) =
|
||||
tagDao.selectTags(file.uri.toString(), file.lastModified)?.intoParsedTags()
|
||||
|
||||
override fun write(rawSongs: Flow<AudioFile>) =
|
||||
rawSongs.onEach { file -> tagDao.updateTags(CachedTags.fromAudioFile(file)) }
|
||||
override suspend fun write(file: DeviceFile, tags: ParsedTags) =
|
||||
tagDao.updateTags(CachedTags.fromParsedTags(file, tags))
|
||||
}
|
||||
|
|
|
@ -28,9 +28,9 @@ import androidx.room.Query
|
|||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
||||
|
||||
|
@ -100,9 +100,8 @@ data class CachedTags(
|
|||
/** @see AudioFile.genreNames */
|
||||
val genreNames: List<String> = listOf()
|
||||
) {
|
||||
fun toAudioFile(deviceFile: DeviceFile) =
|
||||
AudioFile(
|
||||
deviceFile = deviceFile,
|
||||
fun intoParsedTags() =
|
||||
ParsedTags(
|
||||
musicBrainzId = musicBrainzId,
|
||||
name = name,
|
||||
sortName = sortName,
|
||||
|
@ -139,30 +138,30 @@ data class CachedTags(
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun fromAudioFile(audioFile: AudioFile) =
|
||||
fun fromParsedTags(deviceFile: DeviceFile, parsedTags: ParsedTags) =
|
||||
CachedTags(
|
||||
uri = audioFile.deviceFile.uri.toString(),
|
||||
dateModified = audioFile.deviceFile.lastModified,
|
||||
musicBrainzId = audioFile.musicBrainzId,
|
||||
name = audioFile.name,
|
||||
sortName = audioFile.sortName,
|
||||
durationMs = audioFile.durationMs,
|
||||
replayGainTrackAdjustment = audioFile.replayGainTrackAdjustment,
|
||||
replayGainAlbumAdjustment = audioFile.replayGainAlbumAdjustment,
|
||||
track = audioFile.track,
|
||||
disc = audioFile.disc,
|
||||
subtitle = audioFile.subtitle,
|
||||
date = audioFile.date,
|
||||
albumMusicBrainzId = audioFile.albumMusicBrainzId,
|
||||
albumName = audioFile.albumName,
|
||||
albumSortName = audioFile.albumSortName,
|
||||
releaseTypes = audioFile.releaseTypes,
|
||||
artistMusicBrainzIds = audioFile.artistMusicBrainzIds,
|
||||
artistNames = audioFile.artistNames,
|
||||
artistSortNames = audioFile.artistSortNames,
|
||||
albumArtistMusicBrainzIds = audioFile.albumArtistMusicBrainzIds,
|
||||
albumArtistNames = audioFile.albumArtistNames,
|
||||
albumArtistSortNames = audioFile.albumArtistSortNames,
|
||||
genreNames = audioFile.genreNames)
|
||||
uri = deviceFile.uri.toString(),
|
||||
dateModified = deviceFile.lastModified,
|
||||
musicBrainzId = parsedTags.musicBrainzId,
|
||||
name = parsedTags.name,
|
||||
sortName = parsedTags.sortName,
|
||||
durationMs = parsedTags.durationMs,
|
||||
replayGainTrackAdjustment = parsedTags.replayGainTrackAdjustment,
|
||||
replayGainAlbumAdjustment = parsedTags.replayGainAlbumAdjustment,
|
||||
track = parsedTags.track,
|
||||
disc = parsedTags.disc,
|
||||
subtitle = parsedTags.subtitle,
|
||||
date = parsedTags.date,
|
||||
albumMusicBrainzId = parsedTags.albumMusicBrainzId,
|
||||
albumName = parsedTags.albumName,
|
||||
albumSortName = parsedTags.albumSortName,
|
||||
releaseTypes = parsedTags.releaseTypes,
|
||||
artistMusicBrainzIds = parsedTags.artistMusicBrainzIds,
|
||||
artistNames = parsedTags.artistNames,
|
||||
artistSortNames = parsedTags.artistSortNames,
|
||||
albumArtistMusicBrainzIds = parsedTags.albumArtistMusicBrainzIds,
|
||||
albumArtistNames = parsedTags.albumArtistNames,
|
||||
albumArtistSortNames = parsedTags.albumArtistSortNames,
|
||||
genreNames = parsedTags.genreNames)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ModelModule.kt is part of Auxio.
|
||||
* InterpretModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -22,13 +22,13 @@ import android.net.Uri
|
|||
import java.util.UUID
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistHandle
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
import org.oxycblt.auxio.musikr.tag.Disc
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistHandle
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.update
|
||||
|
||||
|
@ -51,7 +51,7 @@ data class PreSong(
|
|||
val preArtists: List<PreArtist>,
|
||||
val preGenres: List<PreGenre>
|
||||
) {
|
||||
val uid =
|
||||
fun computeUid() =
|
||||
musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||
?: Music.UID.auxio(MusicType.SONGS) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
|
@ -77,11 +77,7 @@ data class PreAlbum(
|
|||
val preArtists: List<PreArtist>
|
||||
)
|
||||
|
||||
data class PreArtist(
|
||||
val musicBrainzId: UUID?,
|
||||
val name: Name,
|
||||
val rawName: String?,
|
||||
)
|
||||
data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?)
|
||||
|
||||
data class PreGenre(
|
||||
val name: Name,
|
||||
|
|
|
@ -19,91 +19,88 @@
|
|||
package org.oxycblt.auxio.musikr.tag.interpret
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.tag.Disc
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
||||
interface TagInterpreter {
|
||||
fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
|
||||
fun interpret(file: DeviceFile, parsedTags: ParsedTags, interpretation: Interpretation): PreSong
|
||||
}
|
||||
|
||||
class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||
override fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation) =
|
||||
audioFiles.map { audioFile ->
|
||||
val individualPreArtists =
|
||||
makePreArtists(
|
||||
audioFile.artistMusicBrainzIds,
|
||||
audioFile.artistNames,
|
||||
audioFile.artistSortNames,
|
||||
interpretation)
|
||||
val albumPreArtists =
|
||||
makePreArtists(
|
||||
audioFile.albumArtistMusicBrainzIds,
|
||||
audioFile.albumArtistNames,
|
||||
audioFile.albumArtistSortNames,
|
||||
interpretation)
|
||||
val preAlbum =
|
||||
makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation)
|
||||
val rawArtists =
|
||||
individualPreArtists
|
||||
.ifEmpty { albumPreArtists }
|
||||
.ifEmpty { listOf(unknownPreArtist()) }
|
||||
val rawGenres =
|
||||
makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
||||
val uri = audioFile.deviceFile.uri
|
||||
PreSong(
|
||||
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
|
||||
name =
|
||||
interpretation.nameFactory.parse(
|
||||
need(audioFile, "name", audioFile.name), audioFile.sortName),
|
||||
rawName = audioFile.name,
|
||||
track = audioFile.track,
|
||||
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
|
||||
date = audioFile.date,
|
||||
uri = uri,
|
||||
path = need(audioFile, "path", audioFile.deviceFile.path),
|
||||
mimeType =
|
||||
MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null),
|
||||
size = audioFile.deviceFile.size,
|
||||
durationMs = need(audioFile, "duration", audioFile.durationMs),
|
||||
replayGainAdjustment =
|
||||
ReplayGainAdjustment(
|
||||
audioFile.replayGainTrackAdjustment,
|
||||
audioFile.replayGainAlbumAdjustment,
|
||||
),
|
||||
lastModified = audioFile.deviceFile.lastModified,
|
||||
// TODO: Figure out what to do with date added
|
||||
dateAdded = audioFile.deviceFile.lastModified,
|
||||
preAlbum = preAlbum,
|
||||
preArtists = rawArtists,
|
||||
preGenres = rawGenres)
|
||||
}
|
||||
|
||||
private fun <T> need(audioFile: AudioFile, what: String, value: T?) =
|
||||
requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" }
|
||||
override fun interpret(
|
||||
file: DeviceFile,
|
||||
parsedTags: ParsedTags,
|
||||
interpretation: Interpretation
|
||||
): PreSong {
|
||||
val individualPreArtists =
|
||||
makePreArtists(
|
||||
parsedTags.artistMusicBrainzIds,
|
||||
parsedTags.artistNames,
|
||||
parsedTags.artistSortNames,
|
||||
interpretation)
|
||||
val albumPreArtists =
|
||||
makePreArtists(
|
||||
parsedTags.albumArtistMusicBrainzIds,
|
||||
parsedTags.albumArtistNames,
|
||||
parsedTags.albumArtistSortNames,
|
||||
interpretation)
|
||||
val preAlbum =
|
||||
makePreAlbum(file, parsedTags, individualPreArtists, albumPreArtists, interpretation)
|
||||
val rawArtists =
|
||||
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
||||
val rawGenres =
|
||||
makePreGenres(parsedTags, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
||||
val uri = file.uri
|
||||
return PreSong(
|
||||
musicBrainzId = parsedTags.musicBrainzId?.toUuidOrNull(),
|
||||
name = interpretation.nameFactory.parse(parsedTags.name, parsedTags.sortName),
|
||||
rawName = parsedTags.name,
|
||||
track = parsedTags.track,
|
||||
disc = parsedTags.disc?.let { Disc(it, parsedTags.subtitle) },
|
||||
date = parsedTags.date,
|
||||
uri = uri,
|
||||
path = file.path,
|
||||
mimeType = MimeType(file.mimeType, null),
|
||||
size = file.size,
|
||||
durationMs = parsedTags.durationMs,
|
||||
replayGainAdjustment =
|
||||
ReplayGainAdjustment(
|
||||
parsedTags.replayGainTrackAdjustment,
|
||||
parsedTags.replayGainAlbumAdjustment,
|
||||
),
|
||||
lastModified = file.lastModified,
|
||||
// TODO: Figure out what to do with date added
|
||||
dateAdded = file.lastModified,
|
||||
preAlbum = preAlbum,
|
||||
preArtists = rawArtists,
|
||||
preGenres = rawGenres)
|
||||
}
|
||||
|
||||
private fun makePreAlbum(
|
||||
audioFile: AudioFile,
|
||||
file: DeviceFile,
|
||||
parsedTags: ParsedTags,
|
||||
individualPreArtists: List<PreArtist>,
|
||||
albumPreArtists: List<PreArtist>,
|
||||
interpretation: Interpretation
|
||||
): PreAlbum {
|
||||
val rawAlbumName = need(audioFile, "album name", audioFile.albumName)
|
||||
// TODO: Make fallbacks for this!
|
||||
val rawAlbumName = requireNotNull(parsedTags.albumName)
|
||||
return PreAlbum(
|
||||
musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = interpretation.nameFactory.parse(rawAlbumName, audioFile.albumSortName),
|
||||
musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = interpretation.nameFactory.parse(rawAlbumName, parsedTags.albumSortName),
|
||||
rawName = rawAlbumName,
|
||||
releaseType =
|
||||
ReleaseType.parse(interpretation.separators.split(audioFile.releaseTypes))
|
||||
ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes))
|
||||
?: ReleaseType.Album(null),
|
||||
preArtists =
|
||||
albumPreArtists
|
||||
|
@ -141,12 +138,12 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
|||
private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null)
|
||||
|
||||
private fun makePreGenres(
|
||||
audioFile: AudioFile,
|
||||
parsedTags: ParsedTags,
|
||||
interpretation: Interpretation
|
||||
): List<PreGenre> {
|
||||
val genreNames =
|
||||
audioFile.genreNames.parseId3GenreNames()
|
||||
?: interpretation.separators.split(audioFile.genreNames)
|
||||
parsedTags.genreNames.parseId3GenreNames()
|
||||
?: interpretation.separators.split(parsedTags.genreNames)
|
||||
return genreNames.map { makePreGenre(it, interpretation) }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* TagFields.kt is part of Auxio.
|
||||
* ExoPlayerTagFields.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,32 +25,32 @@ import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
|
|||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
||||
// Song
|
||||
fun TextTags.musicBrainzId() =
|
||||
fun ExoPlayerTags.musicBrainzId() =
|
||||
(vorbis["musicbrainz_releasetrackid"]
|
||||
?: vorbis["musicbrainz release track id"]
|
||||
?: id3v2["TXXX:musicbrainz release track id"]
|
||||
?: id3v2["TXXX:musicbrainz_releasetrackid"])
|
||||
?.first()
|
||||
|
||||
fun TextTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first()
|
||||
fun ExoPlayerTags.name() = (vorbis["title"] ?: id3v2["TIT2"])?.first()
|
||||
|
||||
fun TextTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first()
|
||||
fun ExoPlayerTags.sortName() = (vorbis["titlesort"] ?: id3v2["TSOT"])?.first()
|
||||
|
||||
// Track.
|
||||
fun TextTags.track() =
|
||||
fun ExoPlayerTags.track() =
|
||||
(parseVorbisPositionField(
|
||||
vorbis["tracknumber"]?.first(),
|
||||
(vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first())
|
||||
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
|
||||
|
||||
// Disc and it's subtitle name.
|
||||
fun TextTags.disc() =
|
||||
fun ExoPlayerTags.disc() =
|
||||
(parseVorbisPositionField(
|
||||
vorbis["discnumber"]?.first(),
|
||||
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
|
||||
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
|
||||
|
||||
fun TextTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
|
||||
fun ExoPlayerTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
|
||||
|
||||
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
||||
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
||||
|
@ -64,7 +64,7 @@ fun TextTags.subtitle() = (vorbis["discsubtitle"] ?: id3v2["TSST"])?.first()
|
|||
// TODO: Show original and normal dates side-by-side
|
||||
// TODO: Handle dates that are in "January" because the actual specific release date
|
||||
// isn't known?
|
||||
fun TextTags.date() =
|
||||
fun ExoPlayerTags.date() =
|
||||
(vorbis["originaldate"]?.run { Date.from(first()) }
|
||||
?: vorbis["date"]?.run { Date.from(first()) }
|
||||
?: vorbis["year"]?.run { Date.from(first()) }
|
||||
|
@ -82,18 +82,18 @@ fun TextTags.date() =
|
|||
?: parseId3v23Date())
|
||||
|
||||
// Album
|
||||
fun TextTags.albumMusicBrainzId() =
|
||||
fun ExoPlayerTags.albumMusicBrainzId() =
|
||||
(vorbis["musicbrainz_albumid"]
|
||||
?: vorbis["musicbrainz album id"]
|
||||
?: id3v2["TXXX:musicbrainz album id"]
|
||||
?: id3v2["TXXX:musicbrainz_albumid"])
|
||||
?.first()
|
||||
|
||||
fun TextTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first()
|
||||
fun ExoPlayerTags.albumName() = (vorbis["album"] ?: id3v2["TALB"])?.first()
|
||||
|
||||
fun TextTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first()
|
||||
fun ExoPlayerTags.albumSortName() = (vorbis["albumsort"] ?: id3v2["TSOA"])?.first()
|
||||
|
||||
fun TextTags.releaseTypes() =
|
||||
fun ExoPlayerTags.releaseTypes() =
|
||||
(vorbis["releasetype"]
|
||||
?: vorbis["musicbrainz album type"]
|
||||
?: id3v2["TXXX:musicbrainz album type"]
|
||||
|
@ -103,20 +103,20 @@ fun TextTags.releaseTypes() =
|
|||
id3v2["GRP1"])
|
||||
|
||||
// Artist
|
||||
fun TextTags.artistMusicBrainzIds() =
|
||||
fun ExoPlayerTags.artistMusicBrainzIds() =
|
||||
(vorbis["musicbrainz_artistid"]
|
||||
?: vorbis["musicbrainz artist id"]
|
||||
?: id3v2["TXXX:musicbrainz artist id"]
|
||||
?: id3v2["TXXX:musicbrainz_artistid"])
|
||||
|
||||
fun TextTags.artistNames() =
|
||||
fun ExoPlayerTags.artistNames() =
|
||||
(vorbis["artists"]
|
||||
?: vorbis["artist"]
|
||||
?: id3v2["TXXX:artists"]
|
||||
?: id3v2["TPE1"]
|
||||
?: id3v2["TXXX:artist"])
|
||||
|
||||
fun TextTags.artistSortNames() =
|
||||
fun ExoPlayerTags.artistSortNames() =
|
||||
(vorbis["artistssort"]
|
||||
?: vorbis["artists_sort"]
|
||||
?: vorbis["artists sort"]
|
||||
|
@ -129,13 +129,13 @@ fun TextTags.artistSortNames() =
|
|||
?: id3v2["artistsort"]
|
||||
?: id3v2["TXXX:artist sort"])
|
||||
|
||||
fun TextTags.albumArtistMusicBrainzIds() =
|
||||
fun ExoPlayerTags.albumArtistMusicBrainzIds() =
|
||||
(vorbis["musicbrainz_albumartistid"]
|
||||
?: vorbis["musicbrainz album artist id"]
|
||||
?: id3v2["TXXX:musicbrainz album artist id"]
|
||||
?: id3v2["TXXX:musicbrainz_albumartistid"])
|
||||
|
||||
fun TextTags.albumArtistNames() =
|
||||
fun ExoPlayerTags.albumArtistNames() =
|
||||
(vorbis["albumartists"]
|
||||
?: vorbis["album_artists"]
|
||||
?: vorbis["album artists"]
|
||||
|
@ -148,7 +148,7 @@ fun TextTags.albumArtistNames() =
|
|||
?: id3v2["TXXX:albumartist"]
|
||||
?: id3v2["TXXX:album artist"])
|
||||
|
||||
fun TextTags.albumArtistSortNames() =
|
||||
fun ExoPlayerTags.albumArtistSortNames() =
|
||||
(vorbis["albumartistssort"]
|
||||
?: vorbis["albumartists_sort"]
|
||||
?: vorbis["albumartists sort"]
|
||||
|
@ -163,10 +163,10 @@ fun TextTags.albumArtistSortNames() =
|
|||
?: id3v2["TXXX:album artist sort"])
|
||||
|
||||
// Genre
|
||||
fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
|
||||
fun ExoPlayerTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
|
||||
|
||||
// Compilation Flag
|
||||
fun TextTags.isCompilation() =
|
||||
fun ExoPlayerTags.isCompilation() =
|
||||
(vorbis["compilation"]
|
||||
?: vorbis["itunescompilation"]
|
||||
?: id3v2["TCMP"] // This is a non-standard itunes extension
|
||||
|
@ -178,17 +178,17 @@ fun TextTags.isCompilation() =
|
|||
}
|
||||
|
||||
// ReplayGain information
|
||||
fun TextTags.replayGainTrackAdjustment() =
|
||||
fun ExoPlayerTags.replayGainTrackAdjustment() =
|
||||
(vorbis["r128_track_gain"]?.parseR128Adjustment()
|
||||
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
|
||||
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||
|
||||
fun TextTags.replayGainAlbumAdjustment() =
|
||||
fun ExoPlayerTags.replayGainAlbumAdjustment() =
|
||||
(vorbis["r128_album_gain"]?.parseR128Adjustment()
|
||||
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
|
||||
?: id3v2["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment())
|
||||
|
||||
private fun TextTags.parseId3v23Date(): Date? {
|
||||
private fun ExoPlayerTags.parseId3v23Date(): Date? {
|
||||
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
||||
// is present.
|
||||
val year =
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* TextTags.kt is part of Auxio.
|
||||
* ExoPlayerTags.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
|||
* @param metadata The [Metadata] to wrap.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class TextTags(metadata: Metadata) {
|
||||
class ExoPlayerTags(metadata: Metadata) {
|
||||
private val _id3v2 = mutableMapOf<String, MutableList<String>>()
|
||||
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
|
||||
val id3v2: Map<String, List<String>>
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DeviceFile.kt is part of Auxio.
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* MediaMetadataTagFields.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,15 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.fs
|
||||
package org.oxycblt.auxio.musikr.tag.parse
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class DeviceFile(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val path: Path,
|
||||
val size: Long,
|
||||
val lastModified: Long
|
||||
)
|
||||
import android.media.MediaMetadataRetriever
|
||||
|
||||
fun MediaMetadataRetriever.durationMs() =
|
||||
extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExploreModule.kt is part of Auxio.
|
||||
* ParseModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.explore
|
||||
package org.oxycblt.auxio.musikr.tag.parse
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
@ -25,6 +25,6 @@ import dagger.hilt.components.SingletonComponent
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ExploreModule {
|
||||
@Binds fun explorer(impl: ExplorerImpl): Explorer
|
||||
interface ParseModule {
|
||||
@Binds fun tagParser(factory: TagParserImpl): TagParser
|
||||
}
|
|
@ -1,8 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ParsedTags.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.tag.parse
|
||||
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
|
||||
data class ParsedTags(
|
||||
val deviceFile: DeviceFile,
|
||||
|
||||
val durationMs: Long,
|
||||
val replayGainTrackAdjustment: Float? = null,
|
||||
val replayGainAlbumAdjustment: Float? = null,
|
||||
val musicBrainzId: String? = null,
|
||||
val name: String,
|
||||
val sortName: String? = null,
|
||||
val track: Int? = null,
|
||||
val disc: Int? = null,
|
||||
val subtitle: String? = null,
|
||||
val date: Date? = null,
|
||||
val albumMusicBrainzId: String? = null,
|
||||
val albumName: String? = null,
|
||||
val albumSortName: String? = null,
|
||||
val releaseTypes: List<String> = listOf(),
|
||||
val artistMusicBrainzIds: List<String> = listOf(),
|
||||
val artistNames: List<String> = listOf(),
|
||||
val artistSortNames: List<String> = listOf(),
|
||||
val albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
val albumArtistNames: List<String> = listOf(),
|
||||
val albumArtistSortNames: List<String> = listOf(),
|
||||
val genreNames: List<String> = listOf()
|
||||
)
|
|
@ -1,4 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* TagParser.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.tag.parse
|
||||
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||
import org.oxycblt.auxio.musikr.metadata.AudioMetadata
|
||||
|
||||
interface TagParser {
|
||||
fun parse(file: DeviceFile, metadata: AudioMetadata): ParsedTags
|
||||
}
|
||||
|
||||
class MissingTagError(what: String) : Error("missing tag: $what")
|
||||
|
||||
class TagParserImpl @Inject constructor() : TagParser {
|
||||
override fun parse(file: DeviceFile, metadata: AudioMetadata): ParsedTags {
|
||||
val exoPlayerMetadata =
|
||||
metadata.exoPlayerFormat?.metadata
|
||||
?: return ParsedTags(
|
||||
durationMs =
|
||||
metadata.mediaMetadataRetriever.durationMs()
|
||||
?: throw MissingTagError("durationMs"),
|
||||
name = file.path.name ?: throw MissingTagError("name"),
|
||||
)
|
||||
val exoPlayerTags = ExoPlayerTags(exoPlayerMetadata)
|
||||
return ParsedTags(
|
||||
durationMs =
|
||||
metadata.mediaMetadataRetriever.durationMs() ?: throw MissingTagError("durationMs"),
|
||||
replayGainTrackAdjustment = exoPlayerTags.replayGainTrackAdjustment(),
|
||||
replayGainAlbumAdjustment = exoPlayerTags.replayGainAlbumAdjustment(),
|
||||
musicBrainzId = exoPlayerTags.musicBrainzId(),
|
||||
name = exoPlayerTags.name() ?: file.path.name ?: throw MissingTagError("name"),
|
||||
sortName = exoPlayerTags.sortName(),
|
||||
track = exoPlayerTags.track(),
|
||||
disc = exoPlayerTags.disc(),
|
||||
subtitle = exoPlayerTags.subtitle(),
|
||||
date = exoPlayerTags.date(),
|
||||
albumMusicBrainzId = exoPlayerTags.albumMusicBrainzId(),
|
||||
albumName = exoPlayerTags.albumName(),
|
||||
albumSortName = exoPlayerTags.albumSortName(),
|
||||
releaseTypes = exoPlayerTags.releaseTypes() ?: listOf(),
|
||||
artistMusicBrainzIds = exoPlayerTags.artistMusicBrainzIds() ?: listOf(),
|
||||
artistNames = exoPlayerTags.artistNames() ?: listOf(),
|
||||
artistSortNames = exoPlayerTags.artistSortNames() ?: listOf(),
|
||||
albumArtistMusicBrainzIds = exoPlayerTags.albumArtistMusicBrainzIds() ?: listOf(),
|
||||
albumArtistNames = exoPlayerTags.albumArtistNames() ?: listOf(),
|
||||
albumArtistSortNames = exoPlayerTags.albumArtistSortNames() ?: listOf(),
|
||||
genreNames = exoPlayerTags.genreNames() ?: listOf())
|
||||
}
|
||||
}
|
|
@ -1,8 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Vorbis.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.tag.util
|
||||
|
||||
import org.oxycblt.auxio.util.positiveOrNull
|
||||
|
||||
|
||||
/**
|
||||
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
|
||||
* (optional) total value delimited by a /.
|
||||
|
|
|
@ -38,9 +38,9 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.music.service.MediaSessionUID
|
||||
import org.oxycblt.auxio.music.service.MusicBrowser
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.playback.state.PlaybackCommand
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
|
|
|
@ -21,10 +21,10 @@ package org.oxycblt.auxio.music.metadata
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
||||
import org.oxycblt.auxio.musikr.tag.util.parseId3v2PositionField
|
||||
import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
|
||||
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
||||
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
||||
|
||||
class TagUtilTest {
|
||||
@Test
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* TextTagsTest.kt is part of Auxio.
|
||||
* TextCachedTagsTest.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,66 +27,67 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.musikr.tag.parse.TextTags
|
||||
import org.oxycblt.auxio.musikr.tag.parse.ExoPlayerTags
|
||||
|
||||
class TextCachedTagsTest {
|
||||
@Test
|
||||
fun textTags_vorbis() {
|
||||
val textTags = TextTags(VORBIS_METADATA)
|
||||
assertTrue(textTags.id3v2.isEmpty())
|
||||
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
||||
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"])
|
||||
assertEquals(listOf("2022"), textTags.vorbis["date"])
|
||||
assertEquals(listOf("ep"), textTags.vorbis["releasetype"])
|
||||
assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"])
|
||||
assertEquals(null, textTags.id3v2["APIC"])
|
||||
val exoPlayerTags = ExoPlayerTags(VORBIS_METADATA)
|
||||
assertTrue(exoPlayerTags.id3v2.isEmpty())
|
||||
assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"])
|
||||
assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"])
|
||||
assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"])
|
||||
assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"])
|
||||
assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"])
|
||||
assertEquals(null, exoPlayerTags.id3v2["APIC"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textTags_id3v2() {
|
||||
val textTags = TextTags(ID3V2_METADATA)
|
||||
assertTrue(textTags.vorbis.isEmpty())
|
||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
||||
val exoPlayerTags = ExoPlayerTags(ID3V2_METADATA)
|
||||
assertTrue(exoPlayerTags.vorbis.isEmpty())
|
||||
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textTags_mp4() {
|
||||
val textTags = TextTags(MP4_METADATA)
|
||||
assertTrue(textTags.vorbis.isEmpty())
|
||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
||||
val exoPlayerTags = ExoPlayerTags(MP4_METADATA)
|
||||
assertTrue(exoPlayerTags.vorbis.isEmpty())
|
||||
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textTags_id3v2_vorbis_combined() {
|
||||
val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
|
||||
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
||||
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"])
|
||||
assertEquals(listOf("2022"), textTags.vorbis["date"])
|
||||
assertEquals(listOf("ep"), textTags.vorbis["releasetype"])
|
||||
assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"])
|
||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
||||
val exoPlayerTags =
|
||||
ExoPlayerTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
|
||||
assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"])
|
||||
assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"])
|
||||
assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"])
|
||||
assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"])
|
||||
assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"])
|
||||
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||
|
||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
||||
assertEquals(null, textTags.id3v2["APIC"])
|
||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||
assertEquals(null, exoPlayerTags.id3v2["APIC"])
|
||||
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
Loading…
Reference in a new issue