music: refactor new stack
This commit is contained in:
parent
517da485e1
commit
ba9ab5a445
47 changed files with 466 additions and 376 deletions
|
@ -34,8 +34,8 @@ import org.oxycblt.auxio.music.info.Date
|
|||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.concatLocalized
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
|
|
@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.dirs.MusicDirectories
|
||||
import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import timber.log.Timber as L
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
|
||||
/**
|
||||
* Represents the configuration for specific directories to filter to/from when loading music.
|
||||
|
|
|
@ -33,8 +33,8 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import timber.log.Timber as L
|
||||
|
|
|
@ -23,10 +23,10 @@ import android.net.Uri
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.stack.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.DocumentPathFactory
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.contentResolverSafe
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,11 +28,11 @@ import java.io.OutputStream
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.fs.Volume
|
||||
import org.oxycblt.auxio.music.stack.fs.VolumeManager
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Volume
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.VolumeManager
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
@ -151,7 +151,8 @@ constructor(
|
|||
else ->
|
||||
listOf(
|
||||
InterpretedPath(Components.parseUnix(path), false),
|
||||
InterpretedPath(Components.parseWindows(path), true))
|
||||
InterpretedPath(Components.parseWindows(path), true)
|
||||
)
|
||||
}
|
||||
|
||||
private fun expandInterpretation(
|
||||
|
|
|
@ -24,7 +24,7 @@ import android.media.MediaFormat
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
@ -119,6 +119,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
|
|||
return AudioProperties(
|
||||
bitrate,
|
||||
sampleRate,
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
package org.oxycblt.auxio.music.metadata
|
||||
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
|
||||
|
||||
/**
|
||||
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.PlaylistFile
|
||||
|
||||
interface Interpreter {
|
||||
suspend fun interpret(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
): MutableLibrary
|
||||
}
|
||||
|
||||
class LinkedSong(private val albumLinkedSong: AlbumInterpreter.LinkedSong) {
|
||||
val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong
|
||||
val album: Linked<AlbumImpl, SongImpl> get() = albumLinkedSong.album
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists
|
||||
val genres: Linked<List<GenreImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
|
||||
}
|
||||
|
||||
typealias LinkedAlbum = ArtistInterpreter.LinkedAlbum
|
||||
|
||||
class InterpreterImpl(
|
||||
private val songInterpreter: SongInterpreter
|
||||
) : Interpreter {
|
||||
override suspend fun interpret(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
): MutableLibrary {
|
||||
val preSongs =
|
||||
songInterpreter.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main)
|
||||
.buffer()
|
||||
val albumInterpreter = makeAlbumTree()
|
||||
val artistInterpreter = makeArtistTree()
|
||||
val genreInterpreter = makeGenreTree()
|
||||
|
||||
val genreLinkedSongs = genreInterpreter.register(preSongs).flowOn(Dispatchers.Main).buffer()
|
||||
val artistLinkedSongs =
|
||||
artistInterpreter.register(genreLinkedSongs).flowOn(Dispatchers.Main).buffer()
|
||||
val albumLinkedSongs =
|
||||
albumInterpreter.register(artistLinkedSongs).flowOn(Dispatchers.Main)
|
||||
val linkedSongs = albumLinkedSongs.map { LinkedSong(it) }.toList()
|
||||
|
||||
val genres = genreInterpreter.resolve()
|
||||
val artists = artistInterpreter.resolve()
|
||||
val albums = albumInterpreter.resolve()
|
||||
val songs = linkedSongs.map { SongImpl(it) }
|
||||
return LibraryImpl(songs, albums, artists, genres)
|
||||
}
|
||||
|
||||
private fun makeAlbumTree(): AlbumInterpreter {
|
||||
}
|
||||
|
||||
private fun makeArtistTree(): ArtistInterpreter {
|
||||
}
|
||||
|
||||
private fun makeGenreTree(): GenreInterpreter {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
interface AlbumInterpreter {
|
||||
suspend fun register(linkedSongs: Flow<ArtistInterpreter.LinkedSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<AlbumImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedArtistSong: ArtistInterpreter.LinkedSong,
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface ArtistInterpreter {
|
||||
suspend fun register(preSong: Flow<GenreInterpreter.LinkedSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<ArtistImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedGenreSong: GenreInterpreter.LinkedSong,
|
||||
val linkedAlbum: LinkedAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
)
|
||||
|
||||
data class LinkedAlbum(
|
||||
val preAlbum: PreAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface GenreInterpreter {
|
||||
suspend fun register(preSong: Flow<PreSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<GenreImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val preSong: PreSong,
|
||||
val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface Linked<P, C> {
|
||||
fun resolve(child: C): P
|
||||
}
|
|
@ -27,7 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.contentResolverSafe
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,29 +20,15 @@ package org.oxycblt.auxio.music.stack
|
|||
|
||||
import android.net.Uri
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.model.Interpretation
|
||||
import org.oxycblt.auxio.music.model.Interpreter
|
||||
import org.oxycblt.auxio.music.model.MutableLibrary
|
||||
import org.oxycblt.auxio.music.stack.cache.TagCache
|
||||
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
|
||||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFiles
|
||||
import org.oxycblt.auxio.music.stack.explore.Explorer
|
||||
import org.oxycblt.auxio.music.stack.interpret.Interpretation
|
||||
import org.oxycblt.auxio.music.stack.interpret.Interpreter
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
|
||||
|
||||
interface Indexer {
|
||||
suspend fun run(
|
||||
|
@ -55,37 +41,14 @@ interface Indexer {
|
|||
class IndexerImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val deviceFiles: DeviceFiles,
|
||||
private val tagCache: TagCache,
|
||||
private val tagExtractor: ExoPlayerTagExtractor,
|
||||
private val explorer: Explorer,
|
||||
private val interpreter: Interpreter
|
||||
) : Indexer {
|
||||
override suspend fun run(
|
||||
uris: List<Uri>,
|
||||
interpretation: Interpretation
|
||||
) = coroutineScope {
|
||||
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
||||
val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (cacheFiles, cacheSongs) = tagRead.results()
|
||||
val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (_, extractorSongs) = tagExtractor.results()
|
||||
val sharedExtractorSongs =
|
||||
extractorSongs.shareIn(
|
||||
CoroutineScope(Dispatchers.Main),
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
replay = Int.MAX_VALUE)
|
||||
val tagWrite =
|
||||
async(Dispatchers.IO) { tagCache.write(sharedExtractorSongs) }
|
||||
val library = async(Dispatchers.Main) { interpreter.interpret(
|
||||
merge(cacheSongs, sharedExtractorSongs), emptyFlow(), interpretation
|
||||
)}
|
||||
tagWrite.await()
|
||||
library.await()
|
||||
}
|
||||
|
||||
private fun Flow<TagResult>.results(): Pair<Flow<DeviceFile>, Flow<AudioFile>> {
|
||||
val files = filterIsInstance<TagResult.Miss>().map { it.file }
|
||||
val songs = filterIsInstance<TagResult.Hit>().map { it.audioFile }
|
||||
return files to songs
|
||||
val audioFiles = explorer.explore(uris).flowOn(Dispatchers.Main).buffer()
|
||||
interpreter.interpret(audioFiles, emptyFlow(), interpretation)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.oxycblt.auxio.music.stack.explore
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.music.stack.Indexer
|
||||
import org.oxycblt.auxio.music.stack.IndexerImpl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ExploreModule {
|
||||
@Binds fun explorer(impl: ExplorerImpl): Explorer
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package org.oxycblt.auxio.music.stack.explore
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.oxycblt.auxio.music.stack.explore.cache.TagCache
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.ExoPlayerTagExtractor
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.TagResult
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.DeviceFiles
|
||||
import javax.inject.Inject
|
||||
|
||||
interface Explorer {
|
||||
fun explore(uris: List<Uri>): Flow<AudioFile>
|
||||
}
|
||||
|
||||
class ExplorerImpl @Inject constructor(
|
||||
private val deviceFiles: DeviceFiles,
|
||||
private val tagCache: TagCache,
|
||||
private val tagExtractor: ExoPlayerTagExtractor
|
||||
) : Explorer {
|
||||
override fun explore(uris: List<Uri>): Flow<AudioFile> {
|
||||
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
||||
val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (cacheFiles, cacheSongs) = tagRead.results()
|
||||
val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (_, extractorSongs) = tagExtractor.results()
|
||||
val writtenExtractorSongs = tagCache.write(extractorSongs).flowOn(Dispatchers.IO).buffer()
|
||||
return merge(cacheSongs, writtenExtractorSongs)
|
||||
}
|
||||
|
||||
private fun Flow<TagResult>.results(): Pair<Flow<DeviceFile>, Flow<AudioFile>> {
|
||||
val files = filterIsInstance<TagResult.Miss>().map { it.file }
|
||||
val songs = filterIsInstance<TagResult.Hit>().map { it.audioFile }
|
||||
return files to songs
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* SongInterpreter.kt is part of Auxio.
|
||||
* Preparer.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack
|
||||
package org.oxycblt.auxio.music.stack.explore
|
||||
|
||||
import android.net.Uri
|
||||
import org.oxycblt.auxio.music.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
|
||||
data class DeviceFile(
|
||||
val uri: Uri,
|
|
@ -16,19 +16,20 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.cache
|
||||
package org.oxycblt.auxio.music.stack.explore.cache
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
||||
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.TagResult
|
||||
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||
|
||||
interface TagCache {
|
||||
fun read(files: Flow<DeviceFile>): Flow<TagResult>
|
||||
|
||||
suspend fun write(rawSongs: Flow<AudioFile>)
|
||||
fun write(rawSongs: Flow<AudioFile>): Flow<AudioFile>
|
||||
}
|
||||
|
||||
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
||||
|
@ -44,7 +45,6 @@ class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun write(rawSongs: Flow<AudioFile>) {
|
||||
rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||
}
|
||||
override fun write(rawSongs: Flow<AudioFile>) =
|
||||
rawSongs.onEach { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* CacheModule.kt is part of Auxio.
|
||||
* TagCacheModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.cache
|
||||
package org.oxycblt.auxio.music.stack.explore.cache
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.cache
|
||||
package org.oxycblt.auxio.music.stack.explore.cache
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
|
@ -28,10 +28,10 @@ import androidx.room.Query
|
|||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
|
||||
|
||||
@Database(entities = [Tags::class], version = 50, exportSchema = false)
|
||||
abstract class TagDatabase : RoomDatabase() {
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.extractor
|
||||
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||
|
||||
import android.os.HandlerThread
|
||||
import androidx.media3.common.MediaItem
|
||||
|
@ -28,8 +28,8 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||
import timber.log.Timber as L
|
||||
|
||||
interface TagResult {
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.extractor
|
||||
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.extractor
|
||||
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.media3.exoplayer.MetadataRetriever
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
* ID3Genre.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.music.stack.explore.extractor
|
||||
|
||||
import org.oxycblt.auxio.util.positiveOrNull
|
||||
|
||||
/// --- GENERIC PARSING ---
|
||||
|
||||
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
|
||||
|
||||
// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have
|
||||
// to deal with the cross-module dependencies of MediaStoreExtractor.
|
||||
|
||||
/**
|
||||
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
|
||||
* the selector.
|
||||
*
|
||||
* @param selector A block that determines if the string should be split at a given character.
|
||||
* @return One or more [String]s split by the selector.
|
||||
*/
|
||||
inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
|
||||
val split = mutableListOf<String>()
|
||||
var currentString = ""
|
||||
var i = 0
|
||||
|
||||
while (i < length) {
|
||||
val a = get(i)
|
||||
val b = getOrNull(i + 1)
|
||||
|
||||
if (selector(a)) {
|
||||
// Non-escaped separator, split the string here, making sure any stray whitespace
|
||||
// is removed.
|
||||
split.add(currentString)
|
||||
currentString = ""
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (b != null && a == '\\' && selector(b)) {
|
||||
// Is an escaped character, add the non-escaped variant and skip two
|
||||
// characters to move on to the next one.
|
||||
currentString += b
|
||||
i += 2
|
||||
} else {
|
||||
// Non-escaped, increment normally.
|
||||
currentString += a
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if (currentString.isNotEmpty()) {
|
||||
// Had an in-progress split string that is now terminated, add it.
|
||||
split.add(currentString)
|
||||
}
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix trailing whitespace or blank contents in a [String].
|
||||
*
|
||||
* @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or
|
||||
* empty.
|
||||
*/
|
||||
fun String.correctWhitespace() = trim().ifBlank { null }
|
||||
|
||||
/**
|
||||
* Fix trailing whitespace or blank contents within a list of [String]s.
|
||||
*
|
||||
* @return A list of non-blank strings with trailing whitespace removed.
|
||||
*/
|
||||
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
|
||||
|
||||
/**
|
||||
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
|
||||
* (optional) total value delimited by a /.
|
||||
*
|
||||
* @return The position value extracted from the string field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*
|
||||
* @see transformPositionField
|
||||
*/
|
||||
fun String.parseId3v2PositionField() =
|
||||
split('/', limit = 2).let {
|
||||
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a vorbis-style position + total field. These fields consist of two fields for the position
|
||||
* and total numbers.
|
||||
*
|
||||
* @param pos The position value, or null if not present.
|
||||
* @param total The total value, if not present.
|
||||
* @return The position value extracted from the field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*
|
||||
* @see transformPositionField
|
||||
*/
|
||||
fun parseVorbisPositionField(pos: String?, total: String?) =
|
||||
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
|
||||
|
||||
|
||||
/**
|
||||
* Transform a raw position + total field into a position a way that tolerates placeholder values.
|
||||
*
|
||||
* @param pos The position value, or null if not present.
|
||||
* @param total The total value, if not present.
|
||||
* @return The position value extracted from the field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*/
|
||||
fun transformPositionField(pos: Int?, total: Int?) =
|
||||
if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) {
|
||||
pos
|
||||
} else {
|
||||
null
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.extractor
|
||||
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||
|
||||
import androidx.media3.common.Metadata
|
||||
import androidx.media3.extractor.metadata.id3.InternalFrame
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.emitAll
|
|||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||
|
||||
interface DeviceFiles {
|
||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaFormat
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.stack.fs
|
||||
package org.oxycblt.auxio.music.stack.explore.fs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DeviceModule.kt is part of Auxio.
|
||||
* InterpretModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
@ -25,8 +25,6 @@ import dagger.hilt.components.SingletonComponent
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ModelModule {
|
||||
interface InterpretModule {
|
||||
@Binds fun interpreter(factory: InterpreterImpl): Interpreter
|
||||
|
||||
@Binds fun preparer(preparerImpl: SongInterpreterImpl): SongInterpreter
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret
|
||||
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
|
@ -0,0 +1,71 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.PlaylistFile
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.AlbumLinker
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.ArtistLinker
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.GenreLinker
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.Linked
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.LinkedSong
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.LibraryImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.Preparer
|
||||
|
||||
interface Interpreter {
|
||||
suspend fun interpret(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
): MutableLibrary
|
||||
}
|
||||
|
||||
class InterpreterImpl(
|
||||
private val preparer: Preparer
|
||||
) : Interpreter {
|
||||
override suspend fun interpret(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
): MutableLibrary {
|
||||
val preSongs =
|
||||
preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main)
|
||||
.buffer()
|
||||
val genreLinker = GenreLinker()
|
||||
val genreLinkedSongs = genreLinker.register(preSongs).flowOn(Dispatchers.Main).buffer()
|
||||
val artistLinker = ArtistLinker()
|
||||
val artistLinkedSongs =
|
||||
artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).buffer()
|
||||
val albumLinker = AlbumLinker()
|
||||
val albumLinkedSongs =
|
||||
albumLinker.register(artistLinkedSongs)
|
||||
.flowOn(Dispatchers.Main)
|
||||
.map { LinkedSongImpl(it) }
|
||||
.toList()
|
||||
val genres = genreLinker.resolve()
|
||||
val artists = artistLinker.resolve()
|
||||
val albums = albumLinker.resolve()
|
||||
val songs = albumLinkedSongs.map { SongImpl(it) }
|
||||
return LibraryImpl(songs, albums, artists, genres)
|
||||
}
|
||||
|
||||
|
||||
private data class LinkedSongImpl(
|
||||
private val albumLinkedSong: AlbumLinker.LinkedSong
|
||||
) : LinkedSong {
|
||||
override val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong
|
||||
override val album: Linked<AlbumImpl, SongImpl> get() = albumLinkedSong.album
|
||||
override val artists: Linked<List<ArtistImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists
|
||||
override val genres: Linked<List<GenreImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret.linker
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
|
||||
|
||||
class AlbumLinker {
|
||||
fun register(linkedSongs: Flow<ArtistLinker.LinkedSong>): Flow<LinkedSong> = emptyFlow()
|
||||
fun resolve(): Collection<AlbumImpl> = setOf()
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedArtistSong: ArtistLinker.LinkedSong,
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret.linker
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum
|
||||
|
||||
|
||||
class ArtistLinker {
|
||||
fun register(preSong: Flow<GenreLinker.LinkedSong>): Flow<LinkedSong> = emptyFlow()
|
||||
fun resolve(): Collection<ArtistImpl> = setOf()
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedGenreSong: GenreLinker.LinkedSong,
|
||||
val linkedAlbum: LinkedAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
)
|
||||
|
||||
data class LinkedAlbum(
|
||||
val preAlbum: PreAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret.linker
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
|
||||
|
||||
class GenreLinker {
|
||||
fun register(preSong: Flow<PreSong>): Flow<LinkedSong> = emptyFlow()
|
||||
fun resolve(): Collection<GenreImpl> = setOf()
|
||||
|
||||
data class LinkedSong(
|
||||
val preSong: PreSong,
|
||||
val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret.linker
|
||||
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreAlbum
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreSong
|
||||
|
||||
interface LinkedSong {
|
||||
val preSong: PreSong
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
}
|
||||
|
||||
interface LinkedAlbum {
|
||||
val preAlbum: PreAlbum
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
}
|
||||
|
||||
interface Linked<P, C> {
|
||||
fun resolve(child: C): P
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret.model
|
||||
|
||||
import org.oxycblt.auxio.image.extractor.ParentCover
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
|
@ -27,6 +27,10 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.LinkedAlbum
|
||||
import org.oxycblt.auxio.music.stack.interpret.linker.LinkedSong
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreArtist
|
||||
import org.oxycblt.auxio.music.stack.interpret.prepare.PreGenre
|
||||
import org.oxycblt.auxio.util.update
|
||||
import kotlin.math.min
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret.model
|
||||
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Album
|
|
@ -1,139 +1,7 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
* TagUtil.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.music.stack.extractor
|
||||
|
||||
import org.oxycblt.auxio.util.positiveOrNull
|
||||
|
||||
/// --- GENERIC PARSING ---
|
||||
|
||||
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
|
||||
|
||||
// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have
|
||||
// to deal with the cross-module dependencies of MediaStoreExtractor.
|
||||
|
||||
/**
|
||||
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
|
||||
* the selector.
|
||||
*
|
||||
* @param selector A block that determines if the string should be split at a given character.
|
||||
* @return One or more [String]s split by the selector.
|
||||
*/
|
||||
inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
|
||||
val split = mutableListOf<String>()
|
||||
var currentString = ""
|
||||
var i = 0
|
||||
|
||||
while (i < length) {
|
||||
val a = get(i)
|
||||
val b = getOrNull(i + 1)
|
||||
|
||||
if (selector(a)) {
|
||||
// Non-escaped separator, split the string here, making sure any stray whitespace
|
||||
// is removed.
|
||||
split.add(currentString)
|
||||
currentString = ""
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (b != null && a == '\\' && selector(b)) {
|
||||
// Is an escaped character, add the non-escaped variant and skip two
|
||||
// characters to move on to the next one.
|
||||
currentString += b
|
||||
i += 2
|
||||
} else {
|
||||
// Non-escaped, increment normally.
|
||||
currentString += a
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if (currentString.isNotEmpty()) {
|
||||
// Had an in-progress split string that is now terminated, add it.
|
||||
split.add(currentString)
|
||||
}
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix trailing whitespace or blank contents in a [String].
|
||||
*
|
||||
* @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or
|
||||
* empty.
|
||||
*/
|
||||
fun String.correctWhitespace() = trim().ifBlank { null }
|
||||
|
||||
/**
|
||||
* Fix trailing whitespace or blank contents within a list of [String]s.
|
||||
*
|
||||
* @return A list of non-blank strings with trailing whitespace removed.
|
||||
*/
|
||||
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
|
||||
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||
|
||||
/// --- ID3v2 PARSING ---
|
||||
|
||||
/**
|
||||
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
|
||||
* (optional) total value delimited by a /.
|
||||
*
|
||||
* @return The position value extracted from the string field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*
|
||||
* @see transformPositionField
|
||||
*/
|
||||
fun String.parseId3v2PositionField() =
|
||||
split('/', limit = 2).let {
|
||||
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a vorbis-style position + total field. These fields consist of two fields for the position
|
||||
* and total numbers.
|
||||
*
|
||||
* @param pos The position value, or null if not present.
|
||||
* @param total The total value, if not present.
|
||||
* @return The position value extracted from the field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*
|
||||
* @see transformPositionField
|
||||
*/
|
||||
fun parseVorbisPositionField(pos: String?, total: String?) =
|
||||
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
|
||||
|
||||
/**
|
||||
* Transform a raw position + total field into a position a way that tolerates placeholder values.
|
||||
*
|
||||
* @param pos The position value, or null if not present.
|
||||
* @param total The total value, if not present.
|
||||
* @return The position value extracted from the field, or null if:
|
||||
* - The position could not be parsed
|
||||
* - The position was zeroed AND the total value was not present/zeroed
|
||||
*/
|
||||
fun transformPositionField(pos: Int?, total: Int?) =
|
||||
if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) {
|
||||
pos
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
|
||||
|
@ -170,7 +38,7 @@ private fun String.parseId3v1Genre(): String? {
|
|||
// try to index the genre table with such.
|
||||
val numeric =
|
||||
toIntOrNull()
|
||||
// Not a numeric value, try some other fixed values.
|
||||
// Not a numeric value, try some other fixed values.
|
||||
?: return when (this) {
|
||||
// CR and RX are not technically ID3v1, but are formatted similarly to a plain
|
||||
// number.
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||
|
||||
import android.net.Uri
|
||||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
|
@ -6,8 +6,8 @@ import org.oxycblt.auxio.music.info.Date
|
|||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import java.util.UUID
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.music.stack.interpret.Interpreter
|
||||
import org.oxycblt.auxio.music.stack.interpret.InterpreterImpl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PrepareModule {
|
||||
@Binds fun prepare(factory: PreparerImpl): Preparer
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -8,20 +8,20 @@ import org.oxycblt.auxio.music.info.Disc
|
|||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.interpret.Interpretation
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
||||
interface SongInterpreter {
|
||||
interface Preparer {
|
||||
fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
|
||||
}
|
||||
|
||||
class SongInterpreterImpl(
|
||||
class PreparerImpl(
|
||||
private val nameFactory: Name.Known.Factory,
|
||||
private val separators: Separators
|
||||
) : SongInterpreter {
|
||||
) : Preparer {
|
||||
override fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation) = audioFiles.map { audioFile ->
|
||||
val individualPreArtists = makePreArtists(
|
||||
audioFile.artistMusicBrainzIds,
|
|
@ -31,10 +31,10 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.music.stack.AudioFile
|
||||
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.stack.cache.TagDao
|
||||
import org.oxycblt.auxio.music.stack.cache.Tags
|
||||
import org.oxycblt.auxio.music.stack.explore.cache.TagDao
|
||||
import org.oxycblt.auxio.music.stack.explore.cache.Tags
|
||||
|
||||
class CacheRepositoryTest {
|
||||
@Test
|
||||
|
|
|
@ -20,11 +20,11 @@ package org.oxycblt.auxio.music.metadata
|
|||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.stack.extractor.parseId3v2PositionField
|
||||
import org.oxycblt.auxio.music.stack.extractor.parseVorbisPositionField
|
||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.parseId3v2PositionField
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.parseVorbisPositionField
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
|
||||
|
||||
class TagUtilTest {
|
||||
@Test
|
||||
|
|
|
@ -27,7 +27,7 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.music.stack.extractor.TextTags
|
||||
import org.oxycblt.auxio.music.stack.explore.extractor.TextTags
|
||||
|
||||
class TextTagsTest {
|
||||
@Test
|
||||
|
|
|
@ -26,13 +26,13 @@ import org.junit.Assert.assertNotEquals
|
|||
import org.junit.Test
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.model.ArtistImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
|
||||
import org.oxycblt.auxio.music.model.DeviceLibraryImpl
|
||||
import org.oxycblt.auxio.music.model.GenreImpl
|
||||
import org.oxycblt.auxio.music.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
|
||||
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||
|
||||
class DeviceLibraryTest {
|
||||
|
||||
|
|
Loading…
Reference in a new issue