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.Disc
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.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
|
||||||
|
|
|
@ -24,7 +24,7 @@ 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.music.dirs.MusicDirectories
|
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 org.oxycblt.auxio.settings.Settings
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
||||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
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.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.dirs
|
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.
|
* 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.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory
|
import org.oxycblt.auxio.music.stack.explore.fs.DocumentPathFactory
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
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
|
||||||
|
|
|
@ -23,10 +23,10 @@ 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.music.stack.fs.Components
|
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||||
import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory
|
import org.oxycblt.auxio.music.stack.explore.fs.DocumentPathFactory
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
|
import org.oxycblt.auxio.music.stack.explore.fs.contentResolverSafe
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,11 +28,11 @@ 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.music.stack.extractor.correctWhitespace
|
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.stack.fs.Components
|
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
import org.oxycblt.auxio.music.stack.fs.Volume
|
import org.oxycblt.auxio.music.stack.explore.fs.Volume
|
||||||
import org.oxycblt.auxio.music.stack.fs.VolumeManager
|
import org.oxycblt.auxio.music.stack.explore.fs.VolumeManager
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
|
@ -151,7 +151,8 @@ 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(
|
||||||
|
|
|
@ -24,7 +24,7 @@ import android.media.MediaFormat
|
||||||
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.Song
|
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
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,6 +119,7 @@ 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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
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
|
* 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 javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
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
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,29 +20,15 @@ package org.oxycblt.auxio.music.stack
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
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.buffer
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import org.oxycblt.auxio.music.stack.explore.Explorer
|
||||||
import kotlinx.coroutines.flow.merge
|
import org.oxycblt.auxio.music.stack.interpret.Interpretation
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import org.oxycblt.auxio.music.stack.interpret.Interpreter
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
|
||||||
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
|
|
||||||
|
|
||||||
interface Indexer {
|
interface Indexer {
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
|
@ -55,37 +41,14 @@ interface Indexer {
|
||||||
class IndexerImpl
|
class IndexerImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val deviceFiles: DeviceFiles,
|
private val explorer: Explorer,
|
||||||
private val tagCache: TagCache,
|
|
||||||
private val tagExtractor: ExoPlayerTagExtractor,
|
|
||||||
private val interpreter: Interpreter
|
private val interpreter: Interpreter
|
||||||
) : Indexer {
|
) : Indexer {
|
||||||
override suspend fun run(
|
override suspend fun run(
|
||||||
uris: List<Uri>,
|
uris: List<Uri>,
|
||||||
interpretation: Interpretation
|
interpretation: Interpretation
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
val audioFiles = explorer.explore(uris).flowOn(Dispatchers.Main).buffer()
|
||||||
val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
|
interpreter.interpret(audioFiles, emptyFlow(), interpretation)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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
|
* 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,12 +16,12 @@
|
||||||
* 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.music.stack
|
package org.oxycblt.auxio.music.stack.explore
|
||||||
|
|
||||||
import android.net.Uri
|
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.info.Date
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
|
|
||||||
data class DeviceFile(
|
data class DeviceFile(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
|
@ -16,19 +16,20 @@
|
||||||
* 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.music.stack.cache
|
package org.oxycblt.auxio.music.stack.explore.cache
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import org.oxycblt.auxio.music.stack.AudioFile
|
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
import org.oxycblt.auxio.music.stack.explore.extractor.TagResult
|
||||||
import org.oxycblt.auxio.music.stack.DeviceFile
|
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||||
|
|
||||||
interface TagCache {
|
interface TagCache {
|
||||||
fun read(files: Flow<DeviceFile>): Flow<TagResult>
|
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 {
|
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>) {
|
override fun write(rawSongs: Flow<AudioFile>) =
|
||||||
rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
rawSongs.onEach { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* 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
|
* 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.music.stack.cache
|
package org.oxycblt.auxio.music.stack.explore.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
|
@ -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.music.stack.cache
|
package org.oxycblt.auxio.music.stack.explore.cache
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
@ -28,10 +28,10 @@ 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.music.stack.AudioFile
|
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||||
import org.oxycblt.auxio.music.info.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
|
||||||
|
|
||||||
@Database(entities = [Tags::class], version = 50, exportSchema = false)
|
@Database(entities = [Tags::class], version = 50, exportSchema = false)
|
||||||
abstract class TagDatabase : RoomDatabase() {
|
abstract class TagDatabase : RoomDatabase() {
|
|
@ -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.music.stack.extractor
|
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||||
|
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
@ -28,8 +28,8 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.oxycblt.auxio.music.stack.AudioFile
|
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||||
import org.oxycblt.auxio.music.stack.DeviceFile
|
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
interface TagResult {
|
interface TagResult {
|
|
@ -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.music.stack.extractor
|
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
|
@ -16,12 +16,12 @@
|
||||||
* 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.music.stack.extractor
|
package org.oxycblt.auxio.music.stack.explore.extractor
|
||||||
|
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import androidx.media3.exoplayer.MetadataRetriever
|
import androidx.media3.exoplayer.MetadataRetriever
|
||||||
import javax.inject.Inject
|
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.music.info.Date
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
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/>.
|
* 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.common.Metadata
|
||||||
import androidx.media3.extractor.metadata.id3.InternalFrame
|
import androidx.media3.extractor.metadata.id3.InternalFrame
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -31,7 +31,7 @@ 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.music.stack.DeviceFile
|
import org.oxycblt.auxio.music.stack.explore.DeviceFile
|
||||||
|
|
||||||
interface DeviceFiles {
|
interface DeviceFiles {
|
||||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
|
@ -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.music.stack.fs
|
package org.oxycblt.auxio.music.stack.explore.fs
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* 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
|
* 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.music.model
|
package org.oxycblt.auxio.music.stack.interpret
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -25,8 +25,6 @@ import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface ModelModule {
|
interface InterpretModule {
|
||||||
@Binds fun interpreter(factory: InterpreterImpl): Interpreter
|
@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.info.Name
|
||||||
import org.oxycblt.auxio.music.metadata.Separators
|
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/>.
|
* 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.image.extractor.ParentCover
|
||||||
import org.oxycblt.auxio.list.sort.Sort
|
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.MusicType
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.info.Date
|
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 org.oxycblt.auxio.util.update
|
||||||
import kotlin.math.min
|
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.Song
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
|
@ -1,139 +1,7 @@
|
||||||
/*
|
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||||
* 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() }
|
|
||||||
|
|
||||||
/// --- ID3v2 PARSING ---
|
/// --- 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
|
* 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.
|
// try to index the genre table with such.
|
||||||
val numeric =
|
val numeric =
|
||||||
toIntOrNull()
|
toIntOrNull()
|
||||||
// Not a numeric value, try some other fixed values.
|
// Not a numeric value, try some other fixed values.
|
||||||
?: return when (this) {
|
?: return when (this) {
|
||||||
// CR and RX are not technically ID3v1, but are formatted similarly to a plain
|
// CR and RX are not technically ID3v1, but are formatted similarly to a plain
|
||||||
// number.
|
// number.
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.music.model
|
package org.oxycblt.auxio.music.stack.interpret.prepare
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.oxycblt.auxio.image.extractor.Cover
|
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.Disc
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||||
import java.util.UUID
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
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.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.metadata.Separators
|
import org.oxycblt.auxio.music.metadata.Separators
|
||||||
import org.oxycblt.auxio.music.stack.AudioFile
|
import org.oxycblt.auxio.music.stack.explore.AudioFile
|
||||||
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
|
import org.oxycblt.auxio.music.stack.explore.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
import org.oxycblt.auxio.music.stack.interpret.Interpretation
|
||||||
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 SongInterpreter {
|
interface Preparer {
|
||||||
fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
|
fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation): Flow<PreSong>
|
||||||
}
|
}
|
||||||
|
|
||||||
class SongInterpreterImpl(
|
class PreparerImpl(
|
||||||
private val nameFactory: Name.Known.Factory,
|
private val nameFactory: Name.Known.Factory,
|
||||||
private val separators: Separators
|
private val separators: Separators
|
||||||
) : SongInterpreter {
|
) : Preparer {
|
||||||
override fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation) = audioFiles.map { audioFile ->
|
override fun prepare(audioFiles: Flow<AudioFile>, interpretation: Interpretation) = audioFiles.map { audioFile ->
|
||||||
val individualPreArtists = makePreArtists(
|
val individualPreArtists = makePreArtists(
|
||||||
audioFile.artistMusicBrainzIds,
|
audioFile.artistMusicBrainzIds,
|
|
@ -31,10 +31,10 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
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.info.Date
|
||||||
import org.oxycblt.auxio.music.stack.cache.TagDao
|
import org.oxycblt.auxio.music.stack.explore.cache.TagDao
|
||||||
import org.oxycblt.auxio.music.stack.cache.Tags
|
import org.oxycblt.auxio.music.stack.explore.cache.Tags
|
||||||
|
|
||||||
class CacheRepositoryTest {
|
class CacheRepositoryTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -20,11 +20,11 @@ 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.music.stack.extractor.correctWhitespace
|
import org.oxycblt.auxio.music.stack.explore.extractor.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
|
import org.oxycblt.auxio.music.stack.explore.extractor.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.music.stack.extractor.parseId3v2PositionField
|
import org.oxycblt.auxio.music.stack.explore.extractor.parseId3v2PositionField
|
||||||
import org.oxycblt.auxio.music.stack.extractor.parseVorbisPositionField
|
import org.oxycblt.auxio.music.stack.explore.extractor.parseVorbisPositionField
|
||||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
import org.oxycblt.auxio.music.stack.explore.extractor.splitEscaped
|
||||||
|
|
||||||
class TagUtilTest {
|
class TagUtilTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -27,7 +27,7 @@ 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.music.stack.extractor.TextTags
|
import org.oxycblt.auxio.music.stack.explore.extractor.TextTags
|
||||||
|
|
||||||
class TextTagsTest {
|
class TextTagsTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -26,13 +26,13 @@ import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
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.model.AlbumImpl
|
import org.oxycblt.auxio.music.stack.interpret.model.AlbumImpl
|
||||||
import org.oxycblt.auxio.music.model.ArtistImpl
|
import org.oxycblt.auxio.music.stack.interpret.model.ArtistImpl
|
||||||
import org.oxycblt.auxio.music.model.DeviceLibraryImpl
|
import org.oxycblt.auxio.music.model.DeviceLibraryImpl
|
||||||
import org.oxycblt.auxio.music.model.GenreImpl
|
import org.oxycblt.auxio.music.stack.interpret.model.GenreImpl
|
||||||
import org.oxycblt.auxio.music.model.SongImpl
|
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
|
||||||
import org.oxycblt.auxio.music.stack.fs.Components
|
import org.oxycblt.auxio.music.stack.explore.fs.Components
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.explore.fs.Path
|
||||||
|
|
||||||
class DeviceLibraryTest {
|
class DeviceLibraryTest {
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue