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.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
|
||||||
import org.oxycblt.auxio.musikr.metadata.AudioProperties
|
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
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.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.replaygain.formatDb
|
import org.oxycblt.auxio.playback.replaygain.formatDb
|
||||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
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.Playlist
|
||||||
import org.oxycblt.auxio.music.PlaylistDecision
|
import org.oxycblt.auxio.music.PlaylistDecision
|
||||||
import org.oxycblt.auxio.music.PlaylistMessage
|
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.IndexingProgress
|
||||||
|
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
||||||
import org.oxycblt.auxio.playback.PlaybackDecision
|
import org.oxycblt.auxio.playback.PlaybackDecision
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
|
|
@ -43,8 +43,8 @@ import kotlinx.coroutines.withContext
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.musikr.cover.Cover
|
|
||||||
import org.oxycblt.auxio.image.stack.CoverRetriever
|
import org.oxycblt.auxio.image.stack.CoverRetriever
|
||||||
|
import org.oxycblt.auxio.musikr.cover.Cover
|
||||||
|
|
||||||
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
||||||
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
|
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 java.io.InputStream
|
||||||
import javax.inject.Inject
|
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.Cover
|
||||||
import org.oxycblt.auxio.musikr.cover.CoverCache
|
import org.oxycblt.auxio.musikr.cover.CoverCache
|
||||||
import org.oxycblt.auxio.image.stack.extractor.CoverExtractor
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
interface CoverRetriever {
|
interface CoverRetriever {
|
||||||
|
|
|
@ -35,6 +35,7 @@ interface CoverSource {
|
||||||
class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) :
|
class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) :
|
||||||
CoverExtractor {
|
CoverExtractor {
|
||||||
override suspend fun extract(cover: Cover.Single): ByteArray? {
|
override suspend fun extract(cover: Cover.Single): ByteArray? {
|
||||||
|
return null
|
||||||
for (coverSource in coverSources.sources) {
|
for (coverSource in coverSources.sources) {
|
||||||
val stream = coverSource.extract(cover.uri)
|
val stream = coverSource.extract(cover.uri)
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
|
|
|
@ -27,14 +27,14 @@ import java.util.UUID
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.musikr.cover.Cover
|
|
||||||
import org.oxycblt.auxio.list.Item
|
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.Date
|
||||||
import org.oxycblt.auxio.musikr.tag.Disc
|
import org.oxycblt.auxio.musikr.tag.Disc
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
import org.oxycblt.auxio.musikr.tag.Name
|
||||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
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.playback.replaygain.ReplayGainAdjustment
|
||||||
import org.oxycblt.auxio.util.concatLocalized
|
import org.oxycblt.auxio.util.concatLocalized
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
|
|
|
@ -25,12 +25,12 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
|
||||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
|
||||||
import org.oxycblt.auxio.musikr.Indexer
|
import org.oxycblt.auxio.musikr.Indexer
|
||||||
import org.oxycblt.auxio.musikr.IndexingProgress
|
import org.oxycblt.auxio.musikr.IndexingProgress
|
||||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
|
||||||
import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
|
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
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,7 +24,7 @@ import androidx.core.content.edit
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
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 org.oxycblt.auxio.settings.Settings
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicLocationsBinding
|
import org.oxycblt.auxio.databinding.DialogMusicLocationsBinding
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
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.ui.ViewBindingMaterialDialogFragment
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
|
@ -20,17 +20,16 @@ package org.oxycblt.auxio.musikr
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.flow
|
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.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 {
|
interface Indexer {
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
|
@ -53,22 +52,19 @@ sealed interface IndexingProgress {
|
||||||
|
|
||||||
class IndexerImpl
|
class IndexerImpl
|
||||||
@Inject
|
@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(
|
override suspend fun run(
|
||||||
uris: List<Uri>,
|
uris: List<Uri>,
|
||||||
interpretation: Interpretation,
|
interpretation: Interpretation,
|
||||||
onProgress: suspend (IndexingProgress) -> Unit
|
onProgress: suspend (IndexingProgress) -> Unit
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
val files = explorer.explore(uris, onProgress)
|
val explored = exploreStep.explore(uris).buffer(Channel.UNLIMITED)
|
||||||
val audioFiles =
|
val extracted = extractStep.extract(explored).buffer(Channel.UNLIMITED)
|
||||||
files.audios
|
evaluateStep.evaluate(interpretation, extracted)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> Flow<T>.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow<T> =
|
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 {
|
sealed interface Cover {
|
||||||
val key: String
|
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 {
|
class Multi(val all: List<Single>) : Cover {
|
||||||
override val key = "multi@${all.hashCode()}"
|
override val key = "multi@${all.hashCode()}"
|
||||||
|
@ -35,7 +42,7 @@ sealed interface Cover {
|
||||||
|
|
||||||
fun nil() = Multi(listOf())
|
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) }
|
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,7 @@ constructor(
|
||||||
val id = coverIdentifier.identify(data)
|
val id = coverIdentifier.identify(data)
|
||||||
coverFiles.write(id, data)
|
coverFiles.write(id, data)
|
||||||
storedCoversDao.setStoredCover(
|
storedCoversDao.setStoredCover(
|
||||||
StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id)
|
StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id))
|
||||||
)
|
|
||||||
return coverFiles.read(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.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.flattenMerge
|
import kotlinx.coroutines.flow.flattenMerge
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.oxycblt.auxio.musikr.fs.path.DocumentPathFactory
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
interface DeviceFiles {
|
interface DeviceFiles {
|
||||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class DeviceFilesImpl
|
class DeviceFilesImpl
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -64,8 +73,7 @@ constructor(
|
||||||
): Flow<DeviceFile> = flow {
|
): Flow<DeviceFile> = flow {
|
||||||
contentResolver.useQuery(
|
contentResolver.useQuery(
|
||||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
||||||
PROJECTION
|
PROJECTION) { cursor ->
|
||||||
) { cursor ->
|
|
||||||
val childUriIndex =
|
val childUriIndex =
|
||||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||||
val displayNameIndex =
|
val displayNameIndex =
|
||||||
|
@ -97,8 +105,7 @@ constructor(
|
||||||
mimeType,
|
mimeType,
|
||||||
newPath,
|
newPath,
|
||||||
size,
|
size,
|
||||||
lastModified)
|
lastModified))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emitAll(recursive.asFlow().flattenMerge())
|
emitAll(recursive.asFlow().flattenMerge())
|
||||||
|
|
|
@ -27,6 +27,9 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
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
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.fs
|
package org.oxycblt.auxio.musikr.fs.path
|
||||||
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -25,6 +25,12 @@ import android.provider.DocumentsContract
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
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.
|
* 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/>.
|
* 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.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
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
|
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
|
package org.oxycblt.auxio.musikr.metadata
|
||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.Metadata
|
|
||||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
|
||||||
|
|
||||||
data class AudioMetadata(
|
data class AudioMetadata(
|
||||||
val file: DeviceFile,
|
val exoPlayerFormat: Format?,
|
||||||
val exoPlayerFormat: Format,
|
|
||||||
val mediaMetadataRetriever: MediaMetadataRetriever
|
val mediaMetadataRetriever: MediaMetadataRetriever
|
||||||
)
|
)
|
||||||
|
|
|
@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
|
||||||
return AudioProperties(
|
return AudioProperties(
|
||||||
bitrate,
|
bitrate,
|
||||||
sampleRate,
|
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
|
package org.oxycblt.auxio.musikr.metadata
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -6,48 +24,38 @@ import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.exoplayer.MetadataRetriever
|
import androidx.media3.exoplayer.MetadataRetriever
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.guava.await
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
interface MetadataExtractor {
|
interface MetadataExtractor {
|
||||||
fun extract(files: Flow<DeviceFile>): Flow<AudioMetadata>
|
suspend fun extract(file: DeviceFile): AudioMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataExtractorImpl @Inject constructor(
|
class MetadataExtractorImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val mediaSourceFactory: MediaSource.Factory
|
private val mediaSourceFactory: MediaSource.Factory
|
||||||
) : MetadataExtractor {
|
) : MetadataExtractor {
|
||||||
override fun extract(files: Flow<DeviceFile>) = files.mapNotNull {
|
override suspend fun extract(file: DeviceFile): AudioMetadata {
|
||||||
val exoPlayerMetadataFuture = MetadataRetriever.retrieveMetadata(
|
val exoPlayerMetadataFuture =
|
||||||
mediaSourceFactory,
|
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(file.uri))
|
||||||
MediaItem.fromUri(it.uri)
|
val mediaMetadataRetriever =
|
||||||
)
|
MediaMetadataRetriever().apply {
|
||||||
val mediaMetadataRetriever = MediaMetadataRetriever().apply {
|
withContext(Dispatchers.IO) { setDataSource(context, file.uri) }
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
setDataSource(context, it.uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val trackGroupArray = exoPlayerMetadataFuture.await()
|
val trackGroupArray = exoPlayerMetadataFuture.await()
|
||||||
if (trackGroupArray.isEmpty) {
|
if (trackGroupArray.isEmpty) {
|
||||||
return@mapNotNull null
|
return AudioMetadata(null, mediaMetadataRetriever)
|
||||||
}
|
}
|
||||||
val trackGroup = trackGroupArray.get(0)
|
val trackGroup = trackGroupArray.get(0)
|
||||||
if (trackGroup.length == 0) {
|
if (trackGroup.length == 0) {
|
||||||
return@mapNotNull null
|
return AudioMetadata(null, mediaMetadataRetriever)
|
||||||
}
|
}
|
||||||
val format = trackGroup.getFormat(0)
|
val format = trackGroup.getFormat(0)
|
||||||
AudioMetadata(
|
return AudioMetadata(format, mediaMetadataRetriever)
|
||||||
it,
|
|
||||||
format,
|
|
||||||
mediaMetadataRetriever
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,6 +29,5 @@ interface MetadataModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||||
|
|
||||||
@Binds
|
@Binds fun metadataExtractor(extractor: MetadataExtractorImpl): MetadataExtractor
|
||||||
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
|
package org.oxycblt.auxio.musikr.metadata
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
|
@ -16,11 +33,10 @@ import androidx.media3.exoplayer.source.MediaSource
|
||||||
import androidx.media3.exoplayer.source.TrackGroupArray
|
import androidx.media3.exoplayer.source.TrackGroupArray
|
||||||
import androidx.media3.exoplayer.upstream.Allocator
|
import androidx.media3.exoplayer.upstream.Allocator
|
||||||
import androidx.media3.exoplayer.upstream.DefaultAllocator
|
import androidx.media3.exoplayer.upstream.DefaultAllocator
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val MESSAGE_PREPARE = 0
|
private const val MESSAGE_PREPARE = 0
|
||||||
private const val MESSAGE_CONTINUE_LOADING = 1
|
private const val MESSAGE_CONTINUE_LOADING = 1
|
||||||
|
@ -32,6 +48,7 @@ private const val CHECK_INTERVAL_MS = 100
|
||||||
|
|
||||||
interface MetadataRetrieverExt {
|
interface MetadataRetrieverExt {
|
||||||
fun retrieveMetadata(mediaItem: MediaItem): Future<TrackGroupArray>
|
fun retrieveMetadata(mediaItem: MediaItem): Future<TrackGroupArray>
|
||||||
|
|
||||||
fun retrieve()
|
fun retrieve()
|
||||||
|
|
||||||
interface Factory {
|
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 {
|
MetadataRetrieverExt, Handler.Callback {
|
||||||
private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}")
|
private val mediaSourceThread = HandlerThread("Auxio:ChunkedMetadataRetriever:${hashCode()}")
|
||||||
private val mediaSourceHandler: HandlerWrapper
|
private val mediaSourceHandler: HandlerWrapper
|
||||||
private var job: MetadataJob? = null
|
private var job: MetadataJob? = null
|
||||||
|
|
||||||
private data class JobParams(val mediaItem: MediaItem, val future: SettableFuture<TrackGroupArray>)
|
private data class JobParams(
|
||||||
|
val mediaItem: MediaItem,
|
||||||
|
val future: SettableFuture<TrackGroupArray>
|
||||||
|
)
|
||||||
|
|
||||||
private class JobData(
|
private class JobData(
|
||||||
val params: JobParams,
|
val params: JobParams,
|
||||||
|
@ -64,7 +86,9 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
||||||
val job = job
|
val job = job
|
||||||
check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
|
check(job == null || job.data.params.future.isDone) { "Already working on something: $job" }
|
||||||
val future = SettableFuture.create<TrackGroupArray>()
|
val future = SettableFuture.create<TrackGroupArray>()
|
||||||
mediaSourceHandler.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future)).sendToTarget()
|
mediaSourceHandler
|
||||||
|
.obtainMessage(MESSAGE_PREPARE, JobParams(mediaItem, future))
|
||||||
|
.sendToTarget()
|
||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,8 +102,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
||||||
MESSAGE_PREPARE -> {
|
MESSAGE_PREPARE -> {
|
||||||
val params = msg.obj as JobParams
|
val params = msg.obj as JobParams
|
||||||
|
|
||||||
val mediaSource =
|
val mediaSource = mediaSourceFactory.createMediaSource(params.mediaItem)
|
||||||
mediaSourceFactory.createMediaSource(params.mediaItem)
|
|
||||||
val data = JobData(params, mediaSource, null)
|
val data = JobData(params, mediaSource, null)
|
||||||
val mediaSourceCaller = MediaSourceCaller(data)
|
val mediaSourceCaller = MediaSourceCaller(data)
|
||||||
mediaSource.prepareSource(
|
mediaSource.prepareSource(
|
||||||
|
@ -87,8 +110,7 @@ class ReusableMetadataRetrieverImpl @Inject constructor(private val mediaSourceF
|
||||||
job = MetadataJob(data, mediaSourceCaller)
|
job = MetadataJob(data, mediaSourceCaller)
|
||||||
|
|
||||||
mediaSourceHandler.sendEmptyMessageDelayed(
|
mediaSourceHandler.sendEmptyMessageDelayed(
|
||||||
MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS
|
MESSAGE_CHECK_FAILURE, /* delayMs= */ CHECK_INTERVAL_MS)
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,8 @@ package org.oxycblt.auxio.musikr.model.graph
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
|
||||||
import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
|
import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
|
||||||
|
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||||
|
|
||||||
class PlaylistLinker {
|
class PlaylistLinker {
|
||||||
fun register(
|
fun register(
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
package org.oxycblt.auxio.musikr.model.impl
|
package org.oxycblt.auxio.musikr.model.impl
|
||||||
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.musikr.cover.Cover
|
|
||||||
import org.oxycblt.auxio.list.sort.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
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.Music
|
||||||
import org.oxycblt.auxio.music.MusicType
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.musikr.cover.Cover
|
||||||
import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum
|
import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum
|
||||||
import org.oxycblt.auxio.musikr.model.graph.LinkedSong
|
import org.oxycblt.auxio.musikr.model.graph.LinkedSong
|
||||||
import org.oxycblt.auxio.musikr.tag.Date
|
import org.oxycblt.auxio.musikr.tag.Date
|
||||||
|
@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.update
|
||||||
class SongImpl(linkedSong: LinkedSong) : Song {
|
class SongImpl(linkedSong: LinkedSong) : Song {
|
||||||
private val preSong = linkedSong.preSong
|
private val preSong = linkedSong.preSong
|
||||||
|
|
||||||
override val uid = preSong.uid
|
override val uid = preSong.computeUid()
|
||||||
override val name = preSong.name
|
override val name = preSong.name
|
||||||
override val track = preSong.track
|
override val track = preSong.track
|
||||||
override val disc = preSong.disc
|
override val disc = preSong.disc
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.model.impl
|
package org.oxycblt.auxio.musikr.model.impl
|
||||||
|
|
||||||
import org.oxycblt.auxio.musikr.cover.Cover
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
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.model.graph.LinkedPlaylist
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
import org.oxycblt.auxio.musikr.tag.Name
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.model
|
package org.oxycblt.auxio.musikr.pipeline
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -24,12 +24,11 @@ import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
|
||||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
|
||||||
import org.oxycblt.auxio.musikr.model.graph.AlbumLinker
|
import org.oxycblt.auxio.musikr.model.graph.AlbumLinker
|
||||||
import org.oxycblt.auxio.musikr.model.graph.ArtistLinker
|
import org.oxycblt.auxio.musikr.model.graph.ArtistLinker
|
||||||
import org.oxycblt.auxio.musikr.model.graph.GenreLinker
|
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.Interpretation
|
||||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||||
import org.oxycblt.auxio.musikr.tag.interpret.TagInterpreter
|
import org.oxycblt.auxio.musikr.tag.interpret.TagInterpreter
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber
|
||||||
|
|
||||||
interface Modeler {
|
interface EvaluateStep {
|
||||||
suspend fun model(
|
suspend fun evaluate(
|
||||||
audioFiles: Flow<AudioFile>,
|
interpretation: Interpretation,
|
||||||
playlistFiles: Flow<PlaylistFile>,
|
extractedMusic: Flow<ExtractedMusic>
|
||||||
interpretation: Interpretation
|
|
||||||
): MutableLibrary
|
): MutableLibrary
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter) : Modeler {
|
class EvaluateStepImpl
|
||||||
override suspend fun model(
|
@Inject
|
||||||
audioFiles: Flow<AudioFile>,
|
constructor(
|
||||||
playlistFiles: Flow<PlaylistFile>,
|
private val tagInterpreter: TagInterpreter,
|
||||||
interpretation: Interpretation
|
) : EvaluateStep {
|
||||||
|
override suspend fun evaluate(
|
||||||
|
interpretation: Interpretation,
|
||||||
|
extractedMusic: Flow<ExtractedMusic>
|
||||||
): MutableLibrary {
|
): MutableLibrary {
|
||||||
val preSongs =
|
val preSongs =
|
||||||
tagInterpreter
|
extractedMusic
|
||||||
.interpret(audioFiles, interpretation)
|
.filterIsInstance<ExtractedMusic.Song>()
|
||||||
|
.map { tagInterpreter.interpret(it.file, it.tags, interpretation) }
|
||||||
.flowOn(Dispatchers.Main)
|
.flowOn(Dispatchers.Main)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
|
|
||||||
|
@ -91,12 +93,12 @@ class ModelerImpl @Inject constructor(private val tagInterpreter: TagInterpreter
|
||||||
val uidMap = mutableMapOf<Music.UID, SongImpl>()
|
val uidMap = mutableMapOf<Music.UID, SongImpl>()
|
||||||
val songs =
|
val songs =
|
||||||
albumLinkedSongs.mapNotNull {
|
albumLinkedSongs.mapNotNull {
|
||||||
val uid = it.preSong.uid
|
val uid = it.preSong.computeUid()
|
||||||
val other = uidMap[uid]
|
val other = uidMap[uid]
|
||||||
if (other == null) {
|
if (other == null) {
|
||||||
SongImpl(it)
|
SongImpl(it)
|
||||||
} else {
|
} else {
|
||||||
L.d("Song @ $uid already exists at ${other.path}, ignoring")
|
Timber.d("Song @ $uid already exists at ${other.path}, ignoring")
|
||||||
null
|
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
|
* Copyright (c) 2024 Auxio Project
|
||||||
* ModelModule.kt is part of Auxio.
|
* PipelineModule.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.model
|
package org.oxycblt.auxio.musikr.pipeline
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -26,5 +26,9 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface InterpretModule {
|
interface InterpretModule {
|
||||||
@Binds fun interpreter(interpreter: ModelerImpl): Modeler
|
@Binds fun 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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.Playlist
|
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.Components
|
||||||
import org.oxycblt.auxio.musikr.fs.DocumentPathFactory
|
|
||||||
import org.oxycblt.auxio.musikr.fs.Path
|
import org.oxycblt.auxio.musikr.fs.Path
|
||||||
import org.oxycblt.auxio.musikr.fs.contentResolverSafe
|
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
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,8 +39,7 @@ import timber.log.Timber as L
|
||||||
*/
|
*/
|
||||||
interface ExternalPlaylistManager {
|
interface ExternalPlaylistManager {
|
||||||
/**
|
/**
|
||||||
* Import the playli L.d("Unable to extract bit rate field")
|
* Import the playli L.d("Unable to extract bit rate field") st file at the given [uri].
|
||||||
st file at the given [uri].
|
|
||||||
*
|
*
|
||||||
* @param uri The [Uri] of the playlist file to import.
|
* @param uri The [Uri] of the playlist file to import.
|
||||||
* @return An [ImportedPlaylist] containing the paths to the files listed in the playlist file,
|
* @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
|
package org.oxycblt.auxio.musikr.playlist
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,8 +22,6 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3U
|
|
||||||
import org.oxycblt.auxio.musikr.playlist.m3u.M3UImpl
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|
|
@ -28,7 +28,6 @@ import java.io.OutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
|
||||||
import org.oxycblt.auxio.musikr.fs.Components
|
import org.oxycblt.auxio.musikr.fs.Components
|
||||||
import org.oxycblt.auxio.musikr.fs.Path
|
import org.oxycblt.auxio.musikr.fs.Path
|
||||||
import org.oxycblt.auxio.musikr.fs.Volume
|
import org.oxycblt.auxio.musikr.fs.Volume
|
||||||
|
@ -36,6 +35,7 @@ import org.oxycblt.auxio.musikr.fs.VolumeManager
|
||||||
import org.oxycblt.auxio.musikr.playlist.ExportConfig
|
import org.oxycblt.auxio.musikr.playlist.ExportConfig
|
||||||
import org.oxycblt.auxio.musikr.playlist.ImportedPlaylist
|
import org.oxycblt.auxio.musikr.playlist.ImportedPlaylist
|
||||||
import org.oxycblt.auxio.musikr.playlist.PossiblePaths
|
import org.oxycblt.auxio.musikr.playlist.PossiblePaths
|
||||||
|
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
|
@ -154,8 +154,7 @@ constructor(
|
||||||
else ->
|
else ->
|
||||||
listOf(
|
listOf(
|
||||||
InterpretedPath(Components.parseUnix(path), false),
|
InterpretedPath(Components.parseUnix(path), false),
|
||||||
InterpretedPath(Components.parseWindows(path), true)
|
InterpretedPath(Components.parseWindows(path), true))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expandInterpretation(
|
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
|
package org.oxycblt.auxio.musikr.playlist.m3u
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
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
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@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
|
package org.oxycblt.auxio.musikr.tag
|
||||||
|
|
||||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
||||||
|
|
|
@ -19,35 +19,19 @@
|
||||||
package org.oxycblt.auxio.musikr.tag.cache
|
package org.oxycblt.auxio.musikr.tag.cache
|
||||||
|
|
||||||
import javax.inject.Inject
|
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
|
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||||
|
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||||
sealed interface CacheResult {
|
|
||||||
data class Hit(val audioFile: AudioFile) : CacheResult
|
|
||||||
|
|
||||||
data class Miss(val deviceFile: DeviceFile) : CacheResult
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TagCache {
|
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 {
|
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
||||||
override fun read(files: Flow<DeviceFile>) =
|
override suspend fun read(file: DeviceFile) =
|
||||||
files.map { file ->
|
tagDao.selectTags(file.uri.toString(), file.lastModified)?.intoParsedTags()
|
||||||
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
|
|
||||||
if (tags != null) {
|
|
||||||
CacheResult.Hit(tags.toAudioFile(file))
|
|
||||||
} else {
|
|
||||||
CacheResult.Miss(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(rawSongs: Flow<AudioFile>) =
|
override suspend fun write(file: DeviceFile, tags: ParsedTags) =
|
||||||
rawSongs.onEach { file -> tagDao.updateTags(CachedTags.fromAudioFile(file)) }
|
tagDao.updateTags(CachedTags.fromParsedTags(file, tags))
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,9 @@ import androidx.room.Query
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.auxio.musikr.tag.Date
|
|
||||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
|
||||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||||
|
import org.oxycblt.auxio.musikr.tag.Date
|
||||||
|
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||||
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
||||||
|
|
||||||
|
@ -100,9 +100,8 @@ data class CachedTags(
|
||||||
/** @see AudioFile.genreNames */
|
/** @see AudioFile.genreNames */
|
||||||
val genreNames: List<String> = listOf()
|
val genreNames: List<String> = listOf()
|
||||||
) {
|
) {
|
||||||
fun toAudioFile(deviceFile: DeviceFile) =
|
fun intoParsedTags() =
|
||||||
AudioFile(
|
ParsedTags(
|
||||||
deviceFile = deviceFile,
|
|
||||||
musicBrainzId = musicBrainzId,
|
musicBrainzId = musicBrainzId,
|
||||||
name = name,
|
name = name,
|
||||||
sortName = sortName,
|
sortName = sortName,
|
||||||
|
@ -139,30 +138,30 @@ data class CachedTags(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromAudioFile(audioFile: AudioFile) =
|
fun fromParsedTags(deviceFile: DeviceFile, parsedTags: ParsedTags) =
|
||||||
CachedTags(
|
CachedTags(
|
||||||
uri = audioFile.deviceFile.uri.toString(),
|
uri = deviceFile.uri.toString(),
|
||||||
dateModified = audioFile.deviceFile.lastModified,
|
dateModified = deviceFile.lastModified,
|
||||||
musicBrainzId = audioFile.musicBrainzId,
|
musicBrainzId = parsedTags.musicBrainzId,
|
||||||
name = audioFile.name,
|
name = parsedTags.name,
|
||||||
sortName = audioFile.sortName,
|
sortName = parsedTags.sortName,
|
||||||
durationMs = audioFile.durationMs,
|
durationMs = parsedTags.durationMs,
|
||||||
replayGainTrackAdjustment = audioFile.replayGainTrackAdjustment,
|
replayGainTrackAdjustment = parsedTags.replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = audioFile.replayGainAlbumAdjustment,
|
replayGainAlbumAdjustment = parsedTags.replayGainAlbumAdjustment,
|
||||||
track = audioFile.track,
|
track = parsedTags.track,
|
||||||
disc = audioFile.disc,
|
disc = parsedTags.disc,
|
||||||
subtitle = audioFile.subtitle,
|
subtitle = parsedTags.subtitle,
|
||||||
date = audioFile.date,
|
date = parsedTags.date,
|
||||||
albumMusicBrainzId = audioFile.albumMusicBrainzId,
|
albumMusicBrainzId = parsedTags.albumMusicBrainzId,
|
||||||
albumName = audioFile.albumName,
|
albumName = parsedTags.albumName,
|
||||||
albumSortName = audioFile.albumSortName,
|
albumSortName = parsedTags.albumSortName,
|
||||||
releaseTypes = audioFile.releaseTypes,
|
releaseTypes = parsedTags.releaseTypes,
|
||||||
artistMusicBrainzIds = audioFile.artistMusicBrainzIds,
|
artistMusicBrainzIds = parsedTags.artistMusicBrainzIds,
|
||||||
artistNames = audioFile.artistNames,
|
artistNames = parsedTags.artistNames,
|
||||||
artistSortNames = audioFile.artistSortNames,
|
artistSortNames = parsedTags.artistSortNames,
|
||||||
albumArtistMusicBrainzIds = audioFile.albumArtistMusicBrainzIds,
|
albumArtistMusicBrainzIds = parsedTags.albumArtistMusicBrainzIds,
|
||||||
albumArtistNames = audioFile.albumArtistNames,
|
albumArtistNames = parsedTags.albumArtistNames,
|
||||||
albumArtistSortNames = audioFile.albumArtistSortNames,
|
albumArtistSortNames = parsedTags.albumArtistSortNames,
|
||||||
genreNames = audioFile.genreNames)
|
genreNames = parsedTags.genreNames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
|
@ -22,13 +22,13 @@ import android.net.Uri
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicType
|
import org.oxycblt.auxio.music.MusicType
|
||||||
|
import org.oxycblt.auxio.musikr.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.Date
|
||||||
import org.oxycblt.auxio.musikr.tag.Disc
|
import org.oxycblt.auxio.musikr.tag.Disc
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
import org.oxycblt.auxio.musikr.tag.Name
|
||||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
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.playback.replaygain.ReplayGainAdjustment
|
||||||
import org.oxycblt.auxio.util.update
|
import org.oxycblt.auxio.util.update
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ data class PreSong(
|
||||||
val preArtists: List<PreArtist>,
|
val preArtists: List<PreArtist>,
|
||||||
val preGenres: List<PreGenre>
|
val preGenres: List<PreGenre>
|
||||||
) {
|
) {
|
||||||
val uid =
|
fun computeUid() =
|
||||||
musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||||
?: Music.UID.auxio(MusicType.SONGS) {
|
?: Music.UID.auxio(MusicType.SONGS) {
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
// 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>
|
val preArtists: List<PreArtist>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PreArtist(
|
data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?)
|
||||||
val musicBrainzId: UUID?,
|
|
||||||
val name: Name,
|
|
||||||
val rawName: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PreGenre(
|
data class PreGenre(
|
||||||
val name: Name,
|
val name: Name,
|
||||||
|
|
|
@ -19,91 +19,88 @@
|
||||||
package org.oxycblt.auxio.musikr.tag.interpret
|
package org.oxycblt.auxio.musikr.tag.interpret
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
||||||
|
import org.oxycblt.auxio.musikr.fs.MimeType
|
||||||
import org.oxycblt.auxio.musikr.tag.Disc
|
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.Name
|
||||||
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
import org.oxycblt.auxio.musikr.tag.ReleaseType
|
||||||
import org.oxycblt.auxio.musikr.tag.AudioFile
|
import org.oxycblt.auxio.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.auxio.musikr.fs.MimeType
|
|
||||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
|
||||||
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
|
|
||||||
interface TagInterpreter {
|
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 {
|
class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
override fun interpret(audioFiles: Flow<AudioFile>, interpretation: Interpretation) =
|
override fun interpret(
|
||||||
audioFiles.map { audioFile ->
|
file: DeviceFile,
|
||||||
|
parsedTags: ParsedTags,
|
||||||
|
interpretation: Interpretation
|
||||||
|
): PreSong {
|
||||||
val individualPreArtists =
|
val individualPreArtists =
|
||||||
makePreArtists(
|
makePreArtists(
|
||||||
audioFile.artistMusicBrainzIds,
|
parsedTags.artistMusicBrainzIds,
|
||||||
audioFile.artistNames,
|
parsedTags.artistNames,
|
||||||
audioFile.artistSortNames,
|
parsedTags.artistSortNames,
|
||||||
interpretation)
|
interpretation)
|
||||||
val albumPreArtists =
|
val albumPreArtists =
|
||||||
makePreArtists(
|
makePreArtists(
|
||||||
audioFile.albumArtistMusicBrainzIds,
|
parsedTags.albumArtistMusicBrainzIds,
|
||||||
audioFile.albumArtistNames,
|
parsedTags.albumArtistNames,
|
||||||
audioFile.albumArtistSortNames,
|
parsedTags.albumArtistSortNames,
|
||||||
interpretation)
|
interpretation)
|
||||||
val preAlbum =
|
val preAlbum =
|
||||||
makePreAlbum(audioFile, individualPreArtists, albumPreArtists, interpretation)
|
makePreAlbum(file, parsedTags, individualPreArtists, albumPreArtists, interpretation)
|
||||||
val rawArtists =
|
val rawArtists =
|
||||||
individualPreArtists
|
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
||||||
.ifEmpty { albumPreArtists }
|
|
||||||
.ifEmpty { listOf(unknownPreArtist()) }
|
|
||||||
val rawGenres =
|
val rawGenres =
|
||||||
makePreGenres(audioFile, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
makePreGenres(parsedTags, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
||||||
val uri = audioFile.deviceFile.uri
|
val uri = file.uri
|
||||||
PreSong(
|
return PreSong(
|
||||||
musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(),
|
musicBrainzId = parsedTags.musicBrainzId?.toUuidOrNull(),
|
||||||
name =
|
name = interpretation.nameFactory.parse(parsedTags.name, parsedTags.sortName),
|
||||||
interpretation.nameFactory.parse(
|
rawName = parsedTags.name,
|
||||||
need(audioFile, "name", audioFile.name), audioFile.sortName),
|
track = parsedTags.track,
|
||||||
rawName = audioFile.name,
|
disc = parsedTags.disc?.let { Disc(it, parsedTags.subtitle) },
|
||||||
track = audioFile.track,
|
date = parsedTags.date,
|
||||||
disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) },
|
|
||||||
date = audioFile.date,
|
|
||||||
uri = uri,
|
uri = uri,
|
||||||
path = need(audioFile, "path", audioFile.deviceFile.path),
|
path = file.path,
|
||||||
mimeType =
|
mimeType = MimeType(file.mimeType, null),
|
||||||
MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null),
|
size = file.size,
|
||||||
size = audioFile.deviceFile.size,
|
durationMs = parsedTags.durationMs,
|
||||||
durationMs = need(audioFile, "duration", audioFile.durationMs),
|
|
||||||
replayGainAdjustment =
|
replayGainAdjustment =
|
||||||
ReplayGainAdjustment(
|
ReplayGainAdjustment(
|
||||||
audioFile.replayGainTrackAdjustment,
|
parsedTags.replayGainTrackAdjustment,
|
||||||
audioFile.replayGainAlbumAdjustment,
|
parsedTags.replayGainAlbumAdjustment,
|
||||||
),
|
),
|
||||||
lastModified = audioFile.deviceFile.lastModified,
|
lastModified = file.lastModified,
|
||||||
// TODO: Figure out what to do with date added
|
// TODO: Figure out what to do with date added
|
||||||
dateAdded = audioFile.deviceFile.lastModified,
|
dateAdded = file.lastModified,
|
||||||
preAlbum = preAlbum,
|
preAlbum = preAlbum,
|
||||||
preArtists = rawArtists,
|
preArtists = rawArtists,
|
||||||
preGenres = rawGenres)
|
preGenres = rawGenres)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> need(audioFile: AudioFile, what: String, value: T?) =
|
|
||||||
requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" }
|
|
||||||
|
|
||||||
private fun makePreAlbum(
|
private fun makePreAlbum(
|
||||||
audioFile: AudioFile,
|
file: DeviceFile,
|
||||||
|
parsedTags: ParsedTags,
|
||||||
individualPreArtists: List<PreArtist>,
|
individualPreArtists: List<PreArtist>,
|
||||||
albumPreArtists: List<PreArtist>,
|
albumPreArtists: List<PreArtist>,
|
||||||
interpretation: Interpretation
|
interpretation: Interpretation
|
||||||
): PreAlbum {
|
): PreAlbum {
|
||||||
val rawAlbumName = need(audioFile, "album name", audioFile.albumName)
|
// TODO: Make fallbacks for this!
|
||||||
|
val rawAlbumName = requireNotNull(parsedTags.albumName)
|
||||||
return PreAlbum(
|
return PreAlbum(
|
||||||
musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(),
|
musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(),
|
||||||
name = interpretation.nameFactory.parse(rawAlbumName, audioFile.albumSortName),
|
name = interpretation.nameFactory.parse(rawAlbumName, parsedTags.albumSortName),
|
||||||
rawName = rawAlbumName,
|
rawName = rawAlbumName,
|
||||||
releaseType =
|
releaseType =
|
||||||
ReleaseType.parse(interpretation.separators.split(audioFile.releaseTypes))
|
ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes))
|
||||||
?: ReleaseType.Album(null),
|
?: ReleaseType.Album(null),
|
||||||
preArtists =
|
preArtists =
|
||||||
albumPreArtists
|
albumPreArtists
|
||||||
|
@ -141,12 +138,12 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null)
|
private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null)
|
||||||
|
|
||||||
private fun makePreGenres(
|
private fun makePreGenres(
|
||||||
audioFile: AudioFile,
|
parsedTags: ParsedTags,
|
||||||
interpretation: Interpretation
|
interpretation: Interpretation
|
||||||
): List<PreGenre> {
|
): List<PreGenre> {
|
||||||
val genreNames =
|
val genreNames =
|
||||||
audioFile.genreNames.parseId3GenreNames()
|
parsedTags.genreNames.parseId3GenreNames()
|
||||||
?: interpretation.separators.split(audioFile.genreNames)
|
?: interpretation.separators.split(parsedTags.genreNames)
|
||||||
return genreNames.map { makePreGenre(it, interpretation) }
|
return genreNames.map { makePreGenre(it, interpretation) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,32 +25,32 @@ import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
// Song
|
// Song
|
||||||
fun TextTags.musicBrainzId() =
|
fun ExoPlayerTags.musicBrainzId() =
|
||||||
(vorbis["musicbrainz_releasetrackid"]
|
(vorbis["musicbrainz_releasetrackid"]
|
||||||
?: vorbis["musicbrainz release track id"]
|
?: vorbis["musicbrainz release track id"]
|
||||||
?: id3v2["TXXX:musicbrainz release track id"]
|
?: id3v2["TXXX:musicbrainz release track id"]
|
||||||
?: id3v2["TXXX:musicbrainz_releasetrackid"])
|
?: id3v2["TXXX:musicbrainz_releasetrackid"])
|
||||||
?.first()
|
?.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.
|
// Track.
|
||||||
fun TextTags.track() =
|
fun ExoPlayerTags.track() =
|
||||||
(parseVorbisPositionField(
|
(parseVorbisPositionField(
|
||||||
vorbis["tracknumber"]?.first(),
|
vorbis["tracknumber"]?.first(),
|
||||||
(vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first())
|
(vorbis["totaltracks"] ?: vorbis["tracktotal"] ?: vorbis["trackc"])?.first())
|
||||||
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
|
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
|
||||||
|
|
||||||
// Disc and it's subtitle name.
|
// Disc and it's subtitle name.
|
||||||
fun TextTags.disc() =
|
fun ExoPlayerTags.disc() =
|
||||||
(parseVorbisPositionField(
|
(parseVorbisPositionField(
|
||||||
vorbis["discnumber"]?.first(),
|
vorbis["discnumber"]?.first(),
|
||||||
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
|
(vorbis["totaldiscs"] ?: vorbis["disctotal"] ?: vorbis["discc"])?.run { first() })
|
||||||
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
|
?: 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
|
// 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
|
// 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: Show original and normal dates side-by-side
|
||||||
// TODO: Handle dates that are in "January" because the actual specific release date
|
// TODO: Handle dates that are in "January" because the actual specific release date
|
||||||
// isn't known?
|
// isn't known?
|
||||||
fun TextTags.date() =
|
fun ExoPlayerTags.date() =
|
||||||
(vorbis["originaldate"]?.run { Date.from(first()) }
|
(vorbis["originaldate"]?.run { Date.from(first()) }
|
||||||
?: vorbis["date"]?.run { Date.from(first()) }
|
?: vorbis["date"]?.run { Date.from(first()) }
|
||||||
?: vorbis["year"]?.run { Date.from(first()) }
|
?: vorbis["year"]?.run { Date.from(first()) }
|
||||||
|
@ -82,18 +82,18 @@ fun TextTags.date() =
|
||||||
?: parseId3v23Date())
|
?: parseId3v23Date())
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
fun TextTags.albumMusicBrainzId() =
|
fun ExoPlayerTags.albumMusicBrainzId() =
|
||||||
(vorbis["musicbrainz_albumid"]
|
(vorbis["musicbrainz_albumid"]
|
||||||
?: vorbis["musicbrainz album id"]
|
?: vorbis["musicbrainz album id"]
|
||||||
?: id3v2["TXXX:musicbrainz album id"]
|
?: id3v2["TXXX:musicbrainz album id"]
|
||||||
?: id3v2["TXXX:musicbrainz_albumid"])
|
?: id3v2["TXXX:musicbrainz_albumid"])
|
||||||
?.first()
|
?.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["releasetype"]
|
||||||
?: vorbis["musicbrainz album type"]
|
?: vorbis["musicbrainz album type"]
|
||||||
?: id3v2["TXXX:musicbrainz album type"]
|
?: id3v2["TXXX:musicbrainz album type"]
|
||||||
|
@ -103,20 +103,20 @@ fun TextTags.releaseTypes() =
|
||||||
id3v2["GRP1"])
|
id3v2["GRP1"])
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
fun TextTags.artistMusicBrainzIds() =
|
fun ExoPlayerTags.artistMusicBrainzIds() =
|
||||||
(vorbis["musicbrainz_artistid"]
|
(vorbis["musicbrainz_artistid"]
|
||||||
?: vorbis["musicbrainz artist id"]
|
?: vorbis["musicbrainz artist id"]
|
||||||
?: id3v2["TXXX:musicbrainz artist id"]
|
?: id3v2["TXXX:musicbrainz artist id"]
|
||||||
?: id3v2["TXXX:musicbrainz_artistid"])
|
?: id3v2["TXXX:musicbrainz_artistid"])
|
||||||
|
|
||||||
fun TextTags.artistNames() =
|
fun ExoPlayerTags.artistNames() =
|
||||||
(vorbis["artists"]
|
(vorbis["artists"]
|
||||||
?: vorbis["artist"]
|
?: vorbis["artist"]
|
||||||
?: id3v2["TXXX:artists"]
|
?: id3v2["TXXX:artists"]
|
||||||
?: id3v2["TPE1"]
|
?: id3v2["TPE1"]
|
||||||
?: id3v2["TXXX:artist"])
|
?: id3v2["TXXX:artist"])
|
||||||
|
|
||||||
fun TextTags.artistSortNames() =
|
fun ExoPlayerTags.artistSortNames() =
|
||||||
(vorbis["artistssort"]
|
(vorbis["artistssort"]
|
||||||
?: vorbis["artists_sort"]
|
?: vorbis["artists_sort"]
|
||||||
?: vorbis["artists sort"]
|
?: vorbis["artists sort"]
|
||||||
|
@ -129,13 +129,13 @@ fun TextTags.artistSortNames() =
|
||||||
?: id3v2["artistsort"]
|
?: id3v2["artistsort"]
|
||||||
?: id3v2["TXXX:artist sort"])
|
?: id3v2["TXXX:artist sort"])
|
||||||
|
|
||||||
fun TextTags.albumArtistMusicBrainzIds() =
|
fun ExoPlayerTags.albumArtistMusicBrainzIds() =
|
||||||
(vorbis["musicbrainz_albumartistid"]
|
(vorbis["musicbrainz_albumartistid"]
|
||||||
?: vorbis["musicbrainz album artist id"]
|
?: vorbis["musicbrainz album artist id"]
|
||||||
?: id3v2["TXXX:musicbrainz album artist id"]
|
?: id3v2["TXXX:musicbrainz album artist id"]
|
||||||
?: id3v2["TXXX:musicbrainz_albumartistid"])
|
?: id3v2["TXXX:musicbrainz_albumartistid"])
|
||||||
|
|
||||||
fun TextTags.albumArtistNames() =
|
fun ExoPlayerTags.albumArtistNames() =
|
||||||
(vorbis["albumartists"]
|
(vorbis["albumartists"]
|
||||||
?: vorbis["album_artists"]
|
?: vorbis["album_artists"]
|
||||||
?: vorbis["album artists"]
|
?: vorbis["album artists"]
|
||||||
|
@ -148,7 +148,7 @@ fun TextTags.albumArtistNames() =
|
||||||
?: id3v2["TXXX:albumartist"]
|
?: id3v2["TXXX:albumartist"]
|
||||||
?: id3v2["TXXX:album artist"])
|
?: id3v2["TXXX:album artist"])
|
||||||
|
|
||||||
fun TextTags.albumArtistSortNames() =
|
fun ExoPlayerTags.albumArtistSortNames() =
|
||||||
(vorbis["albumartistssort"]
|
(vorbis["albumartistssort"]
|
||||||
?: vorbis["albumartists_sort"]
|
?: vorbis["albumartists_sort"]
|
||||||
?: vorbis["albumartists sort"]
|
?: vorbis["albumartists sort"]
|
||||||
|
@ -163,10 +163,10 @@ fun TextTags.albumArtistSortNames() =
|
||||||
?: id3v2["TXXX:album artist sort"])
|
?: id3v2["TXXX:album artist sort"])
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
fun TextTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
|
fun ExoPlayerTags.genreNames() = vorbis["genre"] ?: id3v2["TCON"]
|
||||||
|
|
||||||
// Compilation Flag
|
// Compilation Flag
|
||||||
fun TextTags.isCompilation() =
|
fun ExoPlayerTags.isCompilation() =
|
||||||
(vorbis["compilation"]
|
(vorbis["compilation"]
|
||||||
?: vorbis["itunescompilation"]
|
?: vorbis["itunescompilation"]
|
||||||
?: id3v2["TCMP"] // This is a non-standard itunes extension
|
?: id3v2["TCMP"] // This is a non-standard itunes extension
|
||||||
|
@ -178,17 +178,17 @@ fun TextTags.isCompilation() =
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayGain information
|
// ReplayGain information
|
||||||
fun TextTags.replayGainTrackAdjustment() =
|
fun ExoPlayerTags.replayGainTrackAdjustment() =
|
||||||
(vorbis["r128_track_gain"]?.parseR128Adjustment()
|
(vorbis["r128_track_gain"]?.parseR128Adjustment()
|
||||||
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
|
?: vorbis["replaygain_track_gain"]?.parseReplayGainAdjustment()
|
||||||
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
|
?: id3v2["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||||
|
|
||||||
fun TextTags.replayGainAlbumAdjustment() =
|
fun ExoPlayerTags.replayGainAlbumAdjustment() =
|
||||||
(vorbis["r128_album_gain"]?.parseR128Adjustment()
|
(vorbis["r128_album_gain"]?.parseR128Adjustment()
|
||||||
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
|
?: vorbis["replaygain_album_gain"]?.parseReplayGainAdjustment()
|
||||||
?: id3v2["TXXX: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
|
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
||||||
// is present.
|
// is present.
|
||||||
val year =
|
val year =
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
||||||
* @param metadata The [Metadata] to wrap.
|
* @param metadata The [Metadata] to wrap.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class TextTags(metadata: Metadata) {
|
class ExoPlayerTags(metadata: Metadata) {
|
||||||
private val _id3v2 = mutableMapOf<String, MutableList<String>>()
|
private val _id3v2 = mutableMapOf<String, MutableList<String>>()
|
||||||
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
|
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
|
||||||
val id3v2: Map<String, List<String>>
|
val id3v2: Map<String, List<String>>
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2024 Auxio Project
|
||||||
* DeviceFile.kt is part of Auxio.
|
* MediaMetadataTagFields.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,15 +16,9 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.fs
|
package org.oxycblt.auxio.musikr.tag.parse
|
||||||
|
|
||||||
import android.net.Uri
|
import android.media.MediaMetadataRetriever
|
||||||
|
|
||||||
data class DeviceFile(
|
|
||||||
val uri: Uri,
|
|
||||||
val mimeType: String,
|
|
||||||
val path: Path,
|
|
||||||
val size: Long,
|
|
||||||
val lastModified: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
|
fun MediaMetadataRetriever.durationMs() =
|
||||||
|
extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.musikr.explore
|
package org.oxycblt.auxio.musikr.tag.parse
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -25,6 +25,6 @@ import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface ExploreModule {
|
interface ParseModule {
|
||||||
@Binds fun explorer(impl: ExplorerImpl): Explorer
|
@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
|
package org.oxycblt.auxio.musikr.tag.parse
|
||||||
|
|
||||||
import org.oxycblt.auxio.musikr.fs.DeviceFile
|
import org.oxycblt.auxio.musikr.tag.Date
|
||||||
|
|
||||||
data class ParsedTags(
|
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
|
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 {
|
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
|
package org.oxycblt.auxio.musikr.tag.util
|
||||||
|
|
||||||
import org.oxycblt.auxio.util.positiveOrNull
|
import org.oxycblt.auxio.util.positiveOrNull
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
|
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
|
||||||
* (optional) total value delimited by a /.
|
* (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.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.musikr.tag.Name
|
|
||||||
import org.oxycblt.auxio.music.service.MediaSessionUID
|
import org.oxycblt.auxio.music.service.MediaSessionUID
|
||||||
import org.oxycblt.auxio.music.service.MusicBrowser
|
import org.oxycblt.auxio.music.service.MusicBrowser
|
||||||
|
import org.oxycblt.auxio.musikr.tag.Name
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackCommand
|
import org.oxycblt.auxio.playback.state.PlaybackCommand
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
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.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.musikr.tag.util.correctWhitespace
|
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.parseId3v2PositionField
|
||||||
import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
|
import org.oxycblt.auxio.musikr.tag.util.parseVorbisPositionField
|
||||||
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
import org.oxycblt.auxio.musikr.tag.util.splitEscaped
|
||||||
import org.oxycblt.auxio.musikr.tag.util.parseId3GenreNames
|
|
||||||
|
|
||||||
class TagUtilTest {
|
class TagUtilTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -27,66 +27,67 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.musikr.tag.parse.TextTags
|
import org.oxycblt.auxio.musikr.tag.parse.ExoPlayerTags
|
||||||
|
|
||||||
class TextCachedTagsTest {
|
class TextCachedTagsTest {
|
||||||
@Test
|
@Test
|
||||||
fun textTags_vorbis() {
|
fun textTags_vorbis() {
|
||||||
val textTags = TextTags(VORBIS_METADATA)
|
val exoPlayerTags = ExoPlayerTags(VORBIS_METADATA)
|
||||||
assertTrue(textTags.id3v2.isEmpty())
|
assertTrue(exoPlayerTags.id3v2.isEmpty())
|
||||||
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"])
|
||||||
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"])
|
||||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"])
|
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"])
|
||||||
assertEquals(listOf("2022"), textTags.vorbis["date"])
|
assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"])
|
||||||
assertEquals(listOf("ep"), textTags.vorbis["releasetype"])
|
assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"])
|
||||||
assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"])
|
assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"])
|
||||||
assertEquals(null, textTags.id3v2["APIC"])
|
assertEquals(null, exoPlayerTags.id3v2["APIC"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun textTags_id3v2() {
|
fun textTags_id3v2() {
|
||||||
val textTags = TextTags(ID3V2_METADATA)
|
val exoPlayerTags = ExoPlayerTags(ID3V2_METADATA)
|
||||||
assertTrue(textTags.vorbis.isEmpty())
|
assertTrue(exoPlayerTags.vorbis.isEmpty())
|
||||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun textTags_mp4() {
|
fun textTags_mp4() {
|
||||||
val textTags = TextTags(MP4_METADATA)
|
val exoPlayerTags = ExoPlayerTags(MP4_METADATA)
|
||||||
assertTrue(textTags.vorbis.isEmpty())
|
assertTrue(exoPlayerTags.vorbis.isEmpty())
|
||||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun textTags_id3v2_vorbis_combined() {
|
fun textTags_id3v2_vorbis_combined() {
|
||||||
val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
|
val exoPlayerTags =
|
||||||
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
ExoPlayerTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
|
||||||
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
assertEquals(listOf("Wheel"), exoPlayerTags.vorbis["title"])
|
||||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"])
|
assertEquals(listOf("Paraglow"), exoPlayerTags.vorbis["album"])
|
||||||
assertEquals(listOf("2022"), textTags.vorbis["date"])
|
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.vorbis["artist"])
|
||||||
assertEquals(listOf("ep"), textTags.vorbis["releasetype"])
|
assertEquals(listOf("2022"), exoPlayerTags.vorbis["date"])
|
||||||
assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"])
|
assertEquals(listOf("ep"), exoPlayerTags.vorbis["releasetype"])
|
||||||
assertEquals(null, textTags.id3v2["metadata_block_picture"])
|
assertEquals(listOf("+2 dB"), exoPlayerTags.vorbis["replaygain_track_gain"])
|
||||||
|
assertEquals(null, exoPlayerTags.id3v2["metadata_block_picture"])
|
||||||
|
|
||||||
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
|
assertEquals(listOf("Wheel"), exoPlayerTags.id3v2["TIT2"])
|
||||||
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
|
assertEquals(listOf("Paraglow"), exoPlayerTags.id3v2["TALB"])
|
||||||
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
|
assertEquals(listOf("Parannoul", "Asian Glow"), exoPlayerTags.id3v2["TPE1"])
|
||||||
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
|
assertEquals(listOf("2022"), exoPlayerTags.id3v2["TDRC"])
|
||||||
assertEquals(null, textTags.id3v2["APIC"])
|
assertEquals(null, exoPlayerTags.id3v2["APIC"])
|
||||||
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
|
assertEquals(listOf("ep"), exoPlayerTags.id3v2["TXXX:musicbrainz album type"])
|
||||||
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
|
assertEquals(listOf("+2 dB"), exoPlayerTags.id3v2["TXXX:replaygain_track_gain"])
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
Loading…
Reference in a new issue