musikr: standardize internal song data structure
This commit is contained in:
parent
9ab4dc5595
commit
c70c27a7b4
6 changed files with 151 additions and 164 deletions
|
@ -18,15 +18,13 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.cache
|
package org.oxycblt.musikr.cache
|
||||||
|
|
||||||
import org.oxycblt.ktaglib.Properties
|
|
||||||
import org.oxycblt.musikr.cover.Cover
|
|
||||||
import org.oxycblt.musikr.fs.query.DeviceFile
|
import org.oxycblt.musikr.fs.query.DeviceFile
|
||||||
import org.oxycblt.musikr.tag.parse.ParsedTags
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
|
||||||
interface Cache {
|
interface Cache {
|
||||||
suspend fun read(file: DeviceFile): CachedSong?
|
suspend fun read(file: DeviceFile): CacheResult
|
||||||
|
|
||||||
suspend fun write(file: DeviceFile, song: CachedSong)
|
suspend fun write(song: RawSong)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun full(db: CacheDatabase): Cache = FullCache(db.cachedSongsDao())
|
fun full(db: CacheDatabase): Cache = FullCache(db.cachedSongsDao())
|
||||||
|
@ -35,23 +33,25 @@ interface Cache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CachedSong(
|
sealed interface CacheResult {
|
||||||
val parsedTags: ParsedTags,
|
data class Hit(val song: RawSong) : CacheResult
|
||||||
val cover: Cover.Single?,
|
|
||||||
val properties: Properties
|
data class Miss(val file: DeviceFile) : CacheResult
|
||||||
)
|
}
|
||||||
|
|
||||||
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
override suspend fun read(file: DeviceFile) =
|
override suspend fun read(file: DeviceFile) =
|
||||||
cacheInfoDao.selectInfo(file.uri.toString(), file.lastModified)?.intoCachedSong()
|
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
|
||||||
|
CacheResult.Hit(it.intoRawSong(file))
|
||||||
|
} ?: CacheResult.Miss(file)
|
||||||
|
|
||||||
override suspend fun write(file: DeviceFile, song: CachedSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateInfo(CachedInfo.fromCachedSong(file, song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
override suspend fun read(file: DeviceFile) = null
|
override suspend fun read(file: DeviceFile) = CacheResult.Miss(file)
|
||||||
|
|
||||||
override suspend fun write(file: DeviceFile, song: CachedSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateInfo(CachedInfo.fromCachedSong(file, song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,12 +33,13 @@ import androidx.room.TypeConverters
|
||||||
import org.oxycblt.ktaglib.Properties
|
import org.oxycblt.ktaglib.Properties
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.fs.query.DeviceFile
|
import org.oxycblt.musikr.fs.query.DeviceFile
|
||||||
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
import org.oxycblt.musikr.tag.Date
|
import org.oxycblt.musikr.tag.Date
|
||||||
import org.oxycblt.musikr.tag.parse.ParsedTags
|
import org.oxycblt.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.musikr.tag.util.correctWhitespace
|
import org.oxycblt.musikr.tag.util.correctWhitespace
|
||||||
import org.oxycblt.musikr.tag.util.splitEscaped
|
import org.oxycblt.musikr.tag.util.splitEscaped
|
||||||
|
|
||||||
@Database(entities = [CachedInfo::class], version = 50, exportSchema = false)
|
@Database(entities = [CachedSong::class], version = 50, exportSchema = false)
|
||||||
abstract class CacheDatabase : RoomDatabase() {
|
abstract class CacheDatabase : RoomDatabase() {
|
||||||
internal abstract fun cachedSongsDao(): CacheInfoDao
|
internal abstract fun cachedSongsDao(): CacheInfoDao
|
||||||
|
|
||||||
|
@ -53,23 +54,21 @@ abstract class CacheDatabase : RoomDatabase() {
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
internal interface CacheInfoDao {
|
internal interface CacheInfoDao {
|
||||||
@Query("SELECT * FROM CachedInfo WHERE uri = :uri AND dateModified = :dateModified")
|
@Query("SELECT * FROM CachedSong WHERE uri = :uri AND dateModified = :dateModified")
|
||||||
suspend fun selectInfo(uri: String, dateModified: Long): CachedInfo?
|
suspend fun selectSong(uri: String, dateModified: Long): CachedSong?
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateInfo(cachedInfo: CachedInfo)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@TypeConverters(CachedInfo.Converters::class)
|
@TypeConverters(CachedSong.Converters::class)
|
||||||
internal data class CachedInfo(
|
internal data class CachedSong(
|
||||||
/**
|
|
||||||
* The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black
|
|
||||||
* box only used for comparison.
|
|
||||||
*/
|
|
||||||
@PrimaryKey val uri: String,
|
@PrimaryKey val uri: String,
|
||||||
val dateModified: Long,
|
val dateModified: Long,
|
||||||
val replayGainTrackAdjustment: Float?,
|
val mimeType: String,
|
||||||
val replayGainAlbumAdjustment: Float?,
|
val durationMs: Long,
|
||||||
|
val bitrate: Int,
|
||||||
|
val sampleRate: Int,
|
||||||
val musicBrainzId: String?,
|
val musicBrainzId: String?,
|
||||||
val name: String,
|
val name: String,
|
||||||
val sortName: String?,
|
val sortName: String?,
|
||||||
|
@ -88,21 +87,19 @@ internal data class CachedInfo(
|
||||||
val albumArtistNames: List<String>,
|
val albumArtistNames: List<String>,
|
||||||
val albumArtistSortNames: List<String>,
|
val albumArtistSortNames: List<String>,
|
||||||
val genreNames: List<String>,
|
val genreNames: List<String>,
|
||||||
|
val replayGainTrackAdjustment: Float?,
|
||||||
|
val replayGainAlbumAdjustment: Float?,
|
||||||
val cover: Cover.Single?,
|
val cover: Cover.Single?,
|
||||||
val mimeType: String,
|
|
||||||
val durationMs: Long,
|
|
||||||
val bitrate: Int,
|
|
||||||
val sampleRate: Int,
|
|
||||||
) {
|
) {
|
||||||
fun intoCachedSong() =
|
fun intoRawSong(file: DeviceFile) =
|
||||||
CachedSong(
|
RawSong(
|
||||||
|
file,
|
||||||
|
Properties(mimeType, durationMs, bitrate, sampleRate),
|
||||||
ParsedTags(
|
ParsedTags(
|
||||||
musicBrainzId = musicBrainzId,
|
musicBrainzId = musicBrainzId,
|
||||||
name = name,
|
name = name,
|
||||||
sortName = sortName,
|
sortName = sortName,
|
||||||
durationMs = durationMs,
|
durationMs = durationMs,
|
||||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
|
||||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment,
|
|
||||||
track = track,
|
track = track,
|
||||||
disc = disc,
|
disc = disc,
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
|
@ -117,9 +114,10 @@ internal data class CachedInfo(
|
||||||
albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
|
albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
|
||||||
albumArtistNames = albumArtistNames,
|
albumArtistNames = albumArtistNames,
|
||||||
albumArtistSortNames = albumArtistSortNames,
|
albumArtistSortNames = albumArtistSortNames,
|
||||||
genreNames = genreNames),
|
genreNames = genreNames,
|
||||||
cover,
|
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||||
Properties(mimeType, durationMs, bitrate, sampleRate))
|
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||||
|
cover)
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -139,34 +137,34 @@ internal data class CachedInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCachedSong(deviceFile: DeviceFile, cachedSong: CachedSong) =
|
fun fromRawSong(rawSong: RawSong) =
|
||||||
CachedInfo(
|
CachedSong(
|
||||||
uri = deviceFile.uri.toString(),
|
uri = rawSong.file.uri.toString(),
|
||||||
dateModified = deviceFile.lastModified,
|
dateModified = rawSong.file.lastModified,
|
||||||
musicBrainzId = cachedSong.parsedTags.musicBrainzId,
|
musicBrainzId = rawSong.tags.musicBrainzId,
|
||||||
name = cachedSong.parsedTags.name,
|
name = rawSong.tags.name,
|
||||||
sortName = cachedSong.parsedTags.sortName,
|
sortName = rawSong.tags.sortName,
|
||||||
durationMs = cachedSong.parsedTags.durationMs,
|
durationMs = rawSong.tags.durationMs,
|
||||||
replayGainTrackAdjustment = cachedSong.parsedTags.replayGainTrackAdjustment,
|
track = rawSong.tags.track,
|
||||||
replayGainAlbumAdjustment = cachedSong.parsedTags.replayGainAlbumAdjustment,
|
disc = rawSong.tags.disc,
|
||||||
track = cachedSong.parsedTags.track,
|
subtitle = rawSong.tags.subtitle,
|
||||||
disc = cachedSong.parsedTags.disc,
|
date = rawSong.tags.date,
|
||||||
subtitle = cachedSong.parsedTags.subtitle,
|
albumMusicBrainzId = rawSong.tags.albumMusicBrainzId,
|
||||||
date = cachedSong.parsedTags.date,
|
albumName = rawSong.tags.albumName,
|
||||||
albumMusicBrainzId = cachedSong.parsedTags.albumMusicBrainzId,
|
albumSortName = rawSong.tags.albumSortName,
|
||||||
albumName = cachedSong.parsedTags.albumName,
|
releaseTypes = rawSong.tags.releaseTypes,
|
||||||
albumSortName = cachedSong.parsedTags.albumSortName,
|
artistMusicBrainzIds = rawSong.tags.artistMusicBrainzIds,
|
||||||
releaseTypes = cachedSong.parsedTags.releaseTypes,
|
artistNames = rawSong.tags.artistNames,
|
||||||
artistMusicBrainzIds = cachedSong.parsedTags.artistMusicBrainzIds,
|
artistSortNames = rawSong.tags.artistSortNames,
|
||||||
artistNames = cachedSong.parsedTags.artistNames,
|
albumArtistMusicBrainzIds = rawSong.tags.albumArtistMusicBrainzIds,
|
||||||
artistSortNames = cachedSong.parsedTags.artistSortNames,
|
albumArtistNames = rawSong.tags.albumArtistNames,
|
||||||
albumArtistMusicBrainzIds = cachedSong.parsedTags.albumArtistMusicBrainzIds,
|
albumArtistSortNames = rawSong.tags.albumArtistSortNames,
|
||||||
albumArtistNames = cachedSong.parsedTags.albumArtistNames,
|
genreNames = rawSong.tags.genreNames,
|
||||||
albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames,
|
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
|
||||||
genreNames = cachedSong.parsedTags.genreNames,
|
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
|
||||||
cover = cachedSong.cover,
|
cover = rawSong.cover,
|
||||||
mimeType = cachedSong.properties.mimeType,
|
mimeType = rawSong.properties.mimeType,
|
||||||
bitrate = cachedSong.properties.bitrate,
|
bitrate = rawSong.properties.bitrate,
|
||||||
sampleRate = cachedSong.properties.sampleRate)
|
sampleRate = rawSong.properties.sampleRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,10 +53,7 @@ private class EvaluateStepImpl(
|
||||||
val preSongs =
|
val preSongs =
|
||||||
extractedMusic
|
extractedMusic
|
||||||
.filterIsInstance<ExtractedMusic.Song>()
|
.filterIsInstance<ExtractedMusic.Song>()
|
||||||
.map {
|
.map { tagInterpreter.interpret(it.song, interpretation) }
|
||||||
tagInterpreter.interpret(
|
|
||||||
it.file, it.tags, it.cover, it.properties, interpretation)
|
|
||||||
}
|
|
||||||
.flowOn(Dispatchers.Main)
|
.flowOn(Dispatchers.Main)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
val graphBuilder = MusicGraph.builder()
|
val graphBuilder = MusicGraph.builder()
|
||||||
|
|
|
@ -30,13 +30,12 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import org.oxycblt.ktaglib.Properties
|
import org.oxycblt.ktaglib.Properties
|
||||||
import org.oxycblt.musikr.Storage
|
import org.oxycblt.musikr.Storage
|
||||||
import org.oxycblt.musikr.cache.CachedSong
|
import org.oxycblt.musikr.cache.CacheResult
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.fs.query.DeviceFile
|
import org.oxycblt.musikr.fs.query.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.MetadataExtractor
|
import org.oxycblt.musikr.metadata.MetadataExtractor
|
||||||
import org.oxycblt.musikr.tag.parse.ParsedTags
|
import org.oxycblt.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.musikr.tag.parse.TagParser
|
import org.oxycblt.musikr.tag.parse.TagParser
|
||||||
import timber.log.Timber as L
|
|
||||||
|
|
||||||
interface ExtractStep {
|
interface ExtractStep {
|
||||||
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
|
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
|
||||||
|
@ -55,28 +54,27 @@ private class ExtractStepImpl(
|
||||||
val cacheResults =
|
val cacheResults =
|
||||||
nodes
|
nodes
|
||||||
.filterIsInstance<ExploreNode.Audio>()
|
.filterIsInstance<ExploreNode.Audio>()
|
||||||
.map {
|
.map { storage.cache.read(it.file) }
|
||||||
val tags = storage.cache.read(it.file)
|
|
||||||
MaybeCachedSong(it.file, tags)
|
|
||||||
}
|
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
val (cachedSongs, uncachedSongs) =
|
val divertedFlow =
|
||||||
cacheResults.mapPartition {
|
cacheResults.divert {
|
||||||
it.cachedSong?.let { song ->
|
when (it) {
|
||||||
ExtractedMusic.Song(it.file, song.properties, song.parsedTags, song.cover)
|
is CacheResult.Hit -> Divert.Left(it.song)
|
||||||
|
is CacheResult.Miss -> Divert.Right(it.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val split = uncachedSongs.distribute(16)
|
val cachedSongs = divertedFlow.left.map { ExtractedMusic.Song(it) }
|
||||||
|
val uncachedSongs = divertedFlow.right
|
||||||
|
val distributedFlow = uncachedSongs.distribute(16)
|
||||||
val extractedSongs =
|
val extractedSongs =
|
||||||
Array(split.hot.size) { i ->
|
Array(distributedFlow.flows.size) { i ->
|
||||||
split.hot[i]
|
distributedFlow.flows[i]
|
||||||
.mapNotNull { node ->
|
.mapNotNull { file ->
|
||||||
val metadata =
|
val metadata = metadataExtractor.extract(file) ?: return@mapNotNull null
|
||||||
metadataExtractor.extract(node.file) ?: return@mapNotNull null
|
val tags = tagParser.parse(file, metadata)
|
||||||
val tags = tagParser.parse(node.file, metadata)
|
|
||||||
val cover = metadata.cover?.let { storage.storedCovers.write(it) }
|
val cover = metadata.cover?.let { storage.storedCovers.write(it) }
|
||||||
ExtractedMusic.Song(node.file, metadata.properties, tags, cover)
|
RawSong(file, metadata.properties, tags, cover)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
|
@ -84,26 +82,23 @@ private class ExtractStepImpl(
|
||||||
val writtenSongs =
|
val writtenSongs =
|
||||||
merge(*extractedSongs)
|
merge(*extractedSongs)
|
||||||
.map {
|
.map {
|
||||||
storage.cache.write(it.file, CachedSong(it.tags, it.cover, it.properties))
|
storage.cache.write(it)
|
||||||
it
|
ExtractedMusic.Song(it)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
return merge<ExtractedMusic>(
|
return merge<ExtractedMusic>(
|
||||||
cachedSongs,
|
divertedFlow.manager, cachedSongs, distributedFlow.manager, writtenSongs)
|
||||||
split.cold,
|
|
||||||
writtenSongs,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MaybeCachedSong(val file: DeviceFile, val cachedSong: CachedSong?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ExtractedMusic {
|
data class RawSong(
|
||||||
data class Song(
|
|
||||||
val file: DeviceFile,
|
val file: DeviceFile,
|
||||||
val properties: Properties,
|
val properties: Properties,
|
||||||
val tags: ParsedTags,
|
val tags: ParsedTags,
|
||||||
val cover: Cover.Single?
|
val cover: Cover.Single?
|
||||||
) : ExtractedMusic
|
)
|
||||||
|
|
||||||
|
sealed interface ExtractedMusic {
|
||||||
|
data class Song(val song: RawSong) : ExtractedMusic
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,38 +20,47 @@ package org.oxycblt.musikr.pipeline
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.withIndex
|
import kotlinx.coroutines.flow.withIndex
|
||||||
|
|
||||||
data class HotCold<H, C>(val hot: H, val cold: Flow<C>)
|
sealed interface Divert<L, R> {
|
||||||
|
data class Left<L, R>(val value: L) : Divert<L, R>
|
||||||
|
|
||||||
inline fun <T, R> Flow<T>.mapPartition(crossinline predicate: (T) -> R?): HotCold<Flow<R>, T> {
|
data class Right<L, R>(val value: R) : Divert<L, R>
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
passChannel.close()
|
|
||||||
}
|
|
||||||
return HotCold(passFlow, failFlow)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DivertedFlow<L, R>(val manager: Flow<Nothing>, val left: Flow<L>, val right: Flow<R>)
|
||||||
|
|
||||||
|
inline fun <T, L, R> Flow<T>.divert(
|
||||||
|
crossinline predicate: (T) -> Divert<L, R>
|
||||||
|
): DivertedFlow<L, R> {
|
||||||
|
val leftChannel = Channel<L>(Channel.UNLIMITED)
|
||||||
|
val rightChannel = Channel<R>(Channel.UNLIMITED)
|
||||||
|
val managedFlow =
|
||||||
|
flow<Nothing> {
|
||||||
|
collect {
|
||||||
|
when (val result = predicate(it)) {
|
||||||
|
is Divert.Left -> leftChannel.send(result.value)
|
||||||
|
is Divert.Right -> rightChannel.send(result.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leftChannel.close()
|
||||||
|
rightChannel.close()
|
||||||
|
}
|
||||||
|
return DivertedFlow(managedFlow, leftChannel.receiveAsFlow(), rightChannel.receiveAsFlow())
|
||||||
|
}
|
||||||
|
|
||||||
|
class DistributedFlow<T>(val manager: Flow<Nothing>, val flows: Array<Flow<T>>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equally "distributes" the values of some flow across n new flows.
|
* 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
|
* 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.
|
* 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> {
|
fun <T> Flow<T>.distribute(n: Int): DistributedFlow<T> {
|
||||||
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
|
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
|
||||||
val managerFlow =
|
val managerFlow =
|
||||||
flow<Nothing> {
|
flow<Nothing> {
|
||||||
|
@ -64,5 +73,5 @@ fun <T> Flow<T>.distribute(n: Int): HotCold<Array<Flow<T>>, Nothing> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hotFlows = posChannels.map { it.receiveAsFlow() }.toTypedArray()
|
val hotFlows = posChannels.map { it.receiveAsFlow() }.toTypedArray()
|
||||||
return HotCold(hotFlows, managerFlow)
|
return DistributedFlow(managerFlow, hotFlows)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,10 @@ package org.oxycblt.musikr.tag.interpret
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
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
|
||||||
import org.oxycblt.ktaglib.Properties
|
|
||||||
import org.oxycblt.musikr.Interpretation
|
import org.oxycblt.musikr.Interpretation
|
||||||
import org.oxycblt.musikr.cover.Cover
|
|
||||||
import org.oxycblt.musikr.fs.Format
|
import org.oxycblt.musikr.fs.Format
|
||||||
import org.oxycblt.musikr.fs.query.DeviceFile
|
import org.oxycblt.musikr.fs.query.DeviceFile
|
||||||
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
import org.oxycblt.musikr.tag.Disc
|
import org.oxycblt.musikr.tag.Disc
|
||||||
import org.oxycblt.musikr.tag.Name
|
import org.oxycblt.musikr.tag.Name
|
||||||
import org.oxycblt.musikr.tag.ReleaseType
|
import org.oxycblt.musikr.tag.ReleaseType
|
||||||
|
@ -33,13 +32,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.musikr.tag.util.parseId3GenreNames
|
import org.oxycblt.musikr.tag.util.parseId3GenreNames
|
||||||
|
|
||||||
interface TagInterpreter {
|
interface TagInterpreter {
|
||||||
fun interpret(
|
fun interpret(song: RawSong, interpretation: Interpretation): PreSong
|
||||||
file: DeviceFile,
|
|
||||||
parsedTags: ParsedTags,
|
|
||||||
cover: Cover.Single?,
|
|
||||||
properties: Properties,
|
|
||||||
interpretation: Interpretation
|
|
||||||
): PreSong
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(): TagInterpreter = TagInterpreterImpl
|
fun new(): TagInterpreter = TagInterpreterImpl
|
||||||
|
@ -47,56 +40,51 @@ interface TagInterpreter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private data object TagInterpreterImpl : TagInterpreter {
|
private data object TagInterpreterImpl : TagInterpreter {
|
||||||
override fun interpret(
|
override fun interpret(song: RawSong, interpretation: Interpretation): PreSong {
|
||||||
file: DeviceFile,
|
|
||||||
parsedTags: ParsedTags,
|
|
||||||
cover: Cover.Single?,
|
|
||||||
properties: Properties,
|
|
||||||
interpretation: Interpretation
|
|
||||||
): PreSong {
|
|
||||||
val individualPreArtists =
|
val individualPreArtists =
|
||||||
makePreArtists(
|
makePreArtists(
|
||||||
parsedTags.artistMusicBrainzIds,
|
song.tags.artistMusicBrainzIds,
|
||||||
parsedTags.artistNames,
|
song.tags.artistNames,
|
||||||
parsedTags.artistSortNames,
|
song.tags.artistSortNames,
|
||||||
interpretation)
|
interpretation)
|
||||||
val albumPreArtists =
|
val albumPreArtists =
|
||||||
makePreArtists(
|
makePreArtists(
|
||||||
parsedTags.albumArtistMusicBrainzIds,
|
song.tags.albumArtistMusicBrainzIds,
|
||||||
parsedTags.albumArtistNames,
|
song.tags.albumArtistNames,
|
||||||
parsedTags.albumArtistSortNames,
|
song.tags.albumArtistSortNames,
|
||||||
interpretation)
|
interpretation)
|
||||||
val preAlbum =
|
val preAlbum =
|
||||||
makePreAlbum(file, parsedTags, individualPreArtists, albumPreArtists, interpretation)
|
makePreAlbum(
|
||||||
|
song.file, song.tags, individualPreArtists, albumPreArtists, interpretation)
|
||||||
val rawArtists =
|
val rawArtists =
|
||||||
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
||||||
val rawGenres =
|
val rawGenres =
|
||||||
makePreGenres(parsedTags, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
makePreGenres(song.tags, interpretation).ifEmpty { listOf(unknownPreGenre()) }
|
||||||
val uri = file.uri
|
val uri = song.file.uri
|
||||||
return PreSong(
|
return PreSong(
|
||||||
musicBrainzId = parsedTags.musicBrainzId?.toUuidOrNull(),
|
|
||||||
name = interpretation.nameFactory.parse(parsedTags.name, parsedTags.sortName),
|
|
||||||
rawName = parsedTags.name,
|
|
||||||
track = parsedTags.track,
|
|
||||||
disc = parsedTags.disc?.let { Disc(it, parsedTags.subtitle) },
|
|
||||||
date = parsedTags.date,
|
|
||||||
uri = uri,
|
uri = uri,
|
||||||
path = file.path,
|
path = song.file.path,
|
||||||
size = file.size,
|
size = song.file.size,
|
||||||
durationMs = parsedTags.durationMs,
|
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
||||||
|
lastModified = song.file.lastModified,
|
||||||
|
// TODO: Figure out what to do with date added
|
||||||
|
dateAdded = song.file.lastModified,
|
||||||
|
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
||||||
|
name = interpretation.nameFactory.parse(song.tags.name, song.tags.sortName),
|
||||||
|
rawName = song.tags.name,
|
||||||
|
track = song.tags.track,
|
||||||
|
disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) },
|
||||||
|
date = song.tags.date,
|
||||||
|
durationMs = song.tags.durationMs,
|
||||||
replayGainAdjustment =
|
replayGainAdjustment =
|
||||||
ReplayGainAdjustment(
|
ReplayGainAdjustment(
|
||||||
parsedTags.replayGainTrackAdjustment,
|
song.tags.replayGainTrackAdjustment,
|
||||||
parsedTags.replayGainAlbumAdjustment,
|
song.tags.replayGainAlbumAdjustment,
|
||||||
),
|
),
|
||||||
format = Format.infer(file.mimeType, properties.mimeType),
|
|
||||||
lastModified = file.lastModified,
|
|
||||||
// TODO: Figure out what to do with date added
|
|
||||||
dateAdded = file.lastModified,
|
|
||||||
preAlbum = preAlbum,
|
preAlbum = preAlbum,
|
||||||
preArtists = rawArtists,
|
preArtists = rawArtists,
|
||||||
preGenres = rawGenres,
|
preGenres = rawGenres,
|
||||||
cover = cover)
|
cover = song.cover)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makePreAlbum(
|
private fun makePreAlbum(
|
||||||
|
|
Loading…
Reference in a new issue