musikr: cleanup api

This commit is contained in:
Alexander Capehart 2024-12-16 14:33:31 -05:00
parent 14355a1005
commit 0d5abb6407
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
42 changed files with 189 additions and 183 deletions

View file

@ -94,7 +94,7 @@ sealed interface Music {
override fun toString() = "${format.namespace}:${item.intCode.toString(16)}-$uuid"
enum class Item(val intCode: Int) {
internal enum class Item(val intCode: Int) {
// Item used to be MusicType back when the music module was
// part of Auxio, so these old integer codes remain.
// TODO: Introduce new UID format that removes these.
@ -126,7 +126,7 @@ sealed interface Music {
@TypeConverter fun toMusicUid(string: String?) = string?.let(Companion::fromString)
}
companion object {
internal companion object {
/**
* Creates an Auxio-style [UID] of random composition. Used if there is no
* non-subjective, unlikely-to-change metadata of the music.

View file

@ -55,7 +55,7 @@ sealed interface IndexingProgress {
data object Indeterminate : IndexingProgress
}
class MusikrImpl(
private class MusikrImpl(
private val exploreStep: ExploreStep,
private val extractStep: ExtractStep,
private val evaluateStep: EvaluateStep

View file

@ -18,7 +18,7 @@
package org.oxycblt.musikr.cache
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong
interface Cache {

View file

@ -31,7 +31,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.Date

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.cover
import java.security.MessageDigest
interface CoverIdentifier {
internal interface CoverIdentifier {
suspend fun identify(data: ByteArray): String
companion object {

View file

@ -0,0 +1,11 @@
package org.oxycblt.musikr.fs
import android.net.Uri
data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
val size: Long,
val lastModified: Long
)

View file

@ -84,7 +84,7 @@ sealed interface Format {
"audio/wave" to Wav,
)
fun infer(containerMimeType: String, codecMimeType: String): Format {
internal fun infer(containerMimeType: String, codecMimeType: String): Format {
val codecFormat = CODEC_MAP[codecMimeType]
if (codecFormat != null) {
// Codec found, possibly wrap in container.

View file

@ -26,7 +26,7 @@ import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.query.contentResolverSafe
import org.oxycblt.musikr.util.splitEscaped
class MusicLocation internal constructor(val uri: Uri, val path: Path) {
class MusicLocation private constructor(internal val uri: Uri, internal val path: Path) {
override fun equals(other: Any?) = other is MusicLocation && uri == other.uri
override fun hashCode() = 31 * uri.hashCode()

View file

@ -47,14 +47,14 @@ data class Path(
* @param fileName The name of the file to append to the path.
* @return The new [Path] instance.
*/
fun file(fileName: String) = Path(volume, components.child(fileName))
internal fun file(fileName: String) = Path(volume, components.child(fileName))
/**
* Resolves the [Path] in a human-readable format.
*
* @param context [Context] required to obtain human-readable strings.
*/
fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
internal fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
}
sealed interface Volume {
@ -154,9 +154,7 @@ value class Components private constructor(val components: List<String>) {
fun containing(other: Components) = Components(other.components.drop(components.size))
companion object {
fun nil() = Components(listOf())
internal companion object {
/**
* Parses a path string into a [Components] instance by the unix path separator (/).
*

View file

@ -34,7 +34,7 @@ import org.oxycblt.musikr.fs.query.useQuery
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface DocumentPathFactory {
internal interface DocumentPathFactory {
/**
* Unpacks a document URI into a [Path] instance, using [fromDocumentId].
*

View file

@ -29,7 +29,7 @@ import org.oxycblt.musikr.fs.Path
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface MediaStorePathInterpreter {
internal sealed interface MediaStorePathInterpreter {
/**
* Extract a [Path] from the wrapped [Cursor]. This should be called after the cursor has been
* moved to the row that should be interpreted.

View file

@ -46,7 +46,7 @@ private val svApi21GetPathMethod: Method by lazyReflectedMethod(StorageVolume::c
*
* @see StorageManager.getStorageVolumes
*/
val StorageManager.storageVolumesCompat: List<StorageVolume>
internal val StorageManager.storageVolumesCompat: List<StorageVolume>
get() = storageVolumes.toList()
/**
@ -55,7 +55,7 @@ val StorageManager.storageVolumesCompat: List<StorageVolume>
*
* @see StorageVolume.getDirectory
*/
val StorageVolume.directoryCompat: String?
internal val StorageVolume.directoryCompat: String?
@SuppressLint("NewApi")
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@ -76,7 +76,7 @@ val StorageVolume.directoryCompat: String?
* @return A human-readable name for this volume.
*/
@SuppressLint("NewApi")
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
internal fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
/**
* If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
@ -84,7 +84,7 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio
*
* @see StorageVolume.isPrimary
*/
val StorageVolume.isPrimaryCompat: Boolean
internal val StorageVolume.isPrimaryCompat: Boolean
@SuppressLint("NewApi") get() = isPrimary
/**
@ -93,14 +93,14 @@ val StorageVolume.isPrimaryCompat: Boolean
*
* @see StorageVolume.isEmulated
*/
val StorageVolume.isEmulatedCompat: Boolean
internal val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi") get() = isEmulated
/**
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary"
* to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
*/
val StorageVolume.isInternalCompat: Boolean
internal val StorageVolume.isInternalCompat: Boolean
// Must contain the android system AND be an emulated drive, as non-emulated system
// volumes use their UUID instead of primary in MediaStore/Document URIs.
get() = isPrimaryCompat && isEmulatedCompat
@ -111,7 +111,7 @@ val StorageVolume.isInternalCompat: Boolean
*
* @see StorageVolume.getUuid
*/
val StorageVolume.uuidCompat: String?
internal val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi") get() = uuid
/**
@ -120,7 +120,7 @@ val StorageVolume.uuidCompat: String?
*
* @see StorageVolume.getState
*/
val StorageVolume.stateCompat: String
internal val StorageVolume.stateCompat: String
@SuppressLint("NewApi") get() = state
/**
@ -129,7 +129,7 @@ val StorageVolume.stateCompat: String
*
* @see StorageVolume.getMediaStoreVolumeName
*/
val StorageVolume.mediaStoreVolumeNameCompat: String?
internal val StorageVolume.mediaStoreVolumeNameCompat: String?
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mediaStoreVolumeName

View file

@ -25,7 +25,7 @@ import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Volume
/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
interface VolumeManager {
internal interface VolumeManager {
/**
* The internal storage volume of the device.
*

View file

@ -29,10 +29,11 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path
interface DeviceFiles {
internal interface DeviceFiles {
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
companion object {
@ -40,14 +41,6 @@ interface DeviceFiles {
}
}
data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
val size: Long,
val lastModified: Long
)
@OptIn(ExperimentalCoroutinesApi::class)
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
@ -99,7 +92,8 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De
mimeType,
newPath,
size,
lastModified))
lastModified)
)
}
}
emitAll(recursive.asFlow().flattenMerge())

View file

@ -27,7 +27,7 @@ import android.net.Uri
* Get a content resolver that will not mangle MediaStore queries on certain devices. See
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
*/
val Context.contentResolverSafe: ContentResolver
internal val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver
/**
@ -42,7 +42,7 @@ val Context.contentResolverSafe: ContentResolver
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query
*/
fun ContentResolver.safeQuery(
internal fun ContentResolver.safeQuery(
uri: Uri,
projection: Array<out String>,
selector: String? = null,
@ -63,7 +63,7 @@ fun ContentResolver.safeQuery(
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query
*/
inline fun <reified R> ContentResolver.useQuery(
internal inline fun <reified R> ContentResolver.useQuery(
uri: Uri,
projection: Array<out String>,
selector: String? = null,

View file

@ -25,7 +25,7 @@ import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull
data class MusicGraph(
internal data class MusicGraph(
val songVertex: List<SongVertex>,
val albumVertex: List<AlbumVertex>,
val artistVertex: List<ArtistVertex>,
@ -274,7 +274,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
}
}
class SongVertex(
internal class SongVertex(
val preSong: PreSong,
var albumVertex: AlbumVertex,
var artistVertices: MutableList<ArtistVertex>,
@ -283,12 +283,12 @@ class SongVertex(
var tag: Any? = null
}
class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList<ArtistVertex>) {
internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList<ArtistVertex>) {
val songVertices = mutableSetOf<SongVertex>()
var tag: Any? = null
}
class ArtistVertex(
internal class ArtistVertex(
val preArtist: PreArtist,
) {
val songVertices = mutableSetOf<SongVertex>()
@ -297,7 +297,7 @@ class ArtistVertex(
var tag: Any? = null
}
class GenreVertex(val preGenre: PreGenre) {
internal class GenreVertex(val preGenre: PreGenre) {
val songVertices = mutableSetOf<SongVertex>()
val artistVertices = mutableSetOf<ArtistVertex>()
var tag: Any? = null

View file

@ -22,7 +22,7 @@ import android.content.Context
import java.io.FileInputStream
import java.nio.ByteBuffer
class AndroidInputStream(context: Context, fileRef: FileRef) : NativeInputStream {
internal class AndroidInputStream(context: Context, fileRef: FileRef) : NativeInputStream {
private val fileName = fileRef.fileName
private val fd =
requireNotNull(context.contentResolver.openFileDescriptor(fileRef.uri, "r")) {

View file

@ -0,0 +1,43 @@
package org.oxycblt.musikr.metadata
internal data class Metadata(
val id3v2: Map<String, List<String>>,
val xiph: Map<String, List<String>>,
val mp4: Map<String, List<String>>,
val cover: ByteArray?,
val properties: Properties
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Metadata
if (id3v2 != other.id3v2) return false
if (xiph != other.xiph) return false
if (mp4 != other.mp4) return false
if (cover != null) {
if (other.cover == null) return false
if (!cover.contentEquals(other.cover)) return false
} else if (other.cover != null) return false
if (properties != other.properties) return false
return true
}
override fun hashCode(): Int {
var result = id3v2.hashCode()
result = 31 * result + xiph.hashCode()
result = 31 * result + mp4.hashCode()
result = 31 * result + (cover?.contentHashCode() ?: 0)
result = 31 * result + properties.hashCode()
return result
}
}
data class Properties(
val mimeType: String,
val durationMs: Long,
val bitrate: Int,
val sampleRate: Int,
)

View file

@ -21,10 +21,10 @@ package org.oxycblt.musikr.metadata
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.util.unlikelyToBeNull
interface MetadataExtractor {
internal interface MetadataExtractor {
suspend fun extract(file: DeviceFile): Metadata?
companion object {

View file

@ -23,7 +23,7 @@ package org.oxycblt.musikr.metadata
*
* The vast majority of IO shim between Taglib/KTaglib should occur here to minimize JNI calls.
*/
interface NativeInputStream {
internal interface NativeInputStream {
fun name(): String
fun readBlock(length: Long): ByteArray

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
import android.content.Context
import android.net.Uri
object TagLibJNI {
internal object TagLibJNI {
init {
System.loadLibrary("taglib_jni")
}
@ -41,46 +41,4 @@ object TagLibJNI {
private external fun openNative(ioStream: AndroidInputStream): Metadata?
}
data class FileRef(val fileName: String, val uri: Uri)
data class Metadata(
val id3v2: Map<String, List<String>>,
val xiph: Map<String, List<String>>,
val mp4: Map<String, List<String>>,
val cover: ByteArray?,
val properties: Properties
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Metadata
if (id3v2 != other.id3v2) return false
if (xiph != other.xiph) return false
if (mp4 != other.mp4) return false
if (cover != null) {
if (other.cover == null) return false
if (!cover.contentEquals(other.cover)) return false
} else if (other.cover != null) return false
if (properties != other.properties) return false
return true
}
override fun hashCode(): Int {
var result = id3v2.hashCode()
result = 31 * result + xiph.hashCode()
result = 31 * result + mp4.hashCode()
result = 31 * result + (cover?.contentHashCode() ?: 0)
result = 31 * result + properties.hashCode()
return result
}
}
data class Properties(
val mimeType: String,
val durationMs: Long,
val bitrate: Int,
val sampleRate: Int,
)
internal data class FileRef(val fileName: String, val uri: Uri)

View file

@ -27,7 +27,7 @@ import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.update
interface AlbumCore {
internal interface AlbumCore {
val preAlbum: PreAlbum
val songs: List<Song>
@ -39,7 +39,7 @@ interface AlbumCore {
*
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumImpl(private val core: AlbumCore) : Album {
internal class AlbumImpl(private val core: AlbumCore) : Album {
private val preAlbum = core.preAlbum
override val uid =

View file

@ -27,7 +27,7 @@ import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.util.update
interface ArtistCore {
internal interface ArtistCore {
val preArtist: PreArtist
val songs: Set<Song>
val albums: Set<Album>
@ -40,7 +40,7 @@ interface ArtistCore {
*
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistImpl(private val core: ArtistCore) : Artist {
internal class ArtistImpl(private val core: ArtistCore) : Artist {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
core.preArtist.musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.ARTIST, it) }

View file

@ -26,7 +26,7 @@ import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.util.update
interface GenreCore {
internal interface GenreCore {
val preGenre: PreGenre
val songs: Set<Song>
val artists: Set<Artist>
@ -37,7 +37,7 @@ interface GenreCore {
*
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreImpl(private val core: GenreCore) : Genre {
internal class GenreImpl(private val core: GenreCore) : Genre {
override val uid = Music.UID.auxio(Music.UID.Item.GENRE) { update(core.preGenre.rawName) }
override val name = core.preGenre.name

View file

@ -29,7 +29,7 @@ import org.oxycblt.musikr.graph.GenreVertex
import org.oxycblt.musikr.graph.MusicGraph
import org.oxycblt.musikr.graph.SongVertex
interface LibraryFactory {
internal interface LibraryFactory {
fun create(graph: MusicGraph): MutableLibrary
companion object {

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.fs.Path
class LibraryImpl(
internal class LibraryImpl(
override val songs: Collection<SongImpl>,
override val albums: Collection<AlbumImpl>,
override val artists: Collection<ArtistImpl>,

View file

@ -25,13 +25,13 @@ import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.interpret.PrePlaylist
interface PlaylistCore {
internal interface PlaylistCore {
val prePlaylist: PrePlaylist
val handle: PlaylistHandle
val songs: List<Song>
}
class PlaylistImpl(private val core: PlaylistCore) : Playlist {
internal class PlaylistImpl(private val core: PlaylistCore) : Playlist {
override val uid = core.handle.uid
override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs }

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.tag.interpret.PreSong
interface SongCore {
internal interface SongCore {
val preSong: PreSong
fun resolveAlbum(): Album
@ -39,7 +39,7 @@ interface SongCore {
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SongImpl(private val handle: SongCore) : Song {
internal class SongImpl(private val handle: SongCore) : Song {
private val preSong = handle.preSong
override val uid = preSong.computeUid()

View file

@ -31,7 +31,7 @@ import org.oxycblt.musikr.graph.MusicGraph
import org.oxycblt.musikr.model.LibraryFactory
import org.oxycblt.musikr.tag.interpret.TagInterpreter
interface EvaluateStep {
internal interface EvaluateStep {
suspend fun evaluate(
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>

View file

@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.query.DeviceFiles
import org.oxycblt.musikr.playlist.m3u.M3U
interface ExploreStep {
internal interface ExploreStep {
fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
companion object {
@ -51,6 +51,6 @@ private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreSte
.flowOn(Dispatchers.IO)
}
sealed interface ExploreNode {
internal sealed interface ExploreNode {
data class Audio(val file: DeviceFile) : ExploreNode
}

View file

@ -31,13 +31,13 @@ import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser
interface ExtractStep {
internal interface ExtractStep {
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
companion object {
@ -99,6 +99,6 @@ data class RawSong(
val cover: Cover.Single?
)
sealed interface ExtractedMusic {
internal sealed interface ExtractedMusic {
data class Song(val song: RawSong) : ExtractedMusic
}

View file

@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.withIndex
sealed interface Divert<L, R> {
internal sealed interface Divert<L, R> {
data class Left<L, R>(val value: L) : Divert<L, R>
data class Right<L, R>(val value: R) : Divert<L, R>
@ -33,7 +33,7 @@ sealed interface Divert<L, R> {
class DivertedFlow<L, R>(val manager: Flow<Nothing>, val left: Flow<L>, val right: Flow<R>)
inline fun <T, L, R> Flow<T>.divert(
internal inline fun <T, L, R> Flow<T>.divert(
crossinline predicate: (T) -> Divert<L, R>
): DivertedFlow<L, R> {
val leftChannel = Channel<L>(Channel.UNLIMITED)
@ -52,7 +52,7 @@ inline fun <T, L, R> Flow<T>.divert(
return DivertedFlow(managedFlow, leftChannel.receiveAsFlow(), rightChannel.receiveAsFlow())
}
class DistributedFlow<T>(val manager: Flow<Nothing>, val flows: Array<Flow<T>>)
internal class DistributedFlow<T>(val manager: Flow<Nothing>, val flows: Array<Flow<T>>)
/**
* Equally "distributes" the values of some flow across n new flows.
@ -60,7 +60,7 @@ class DistributedFlow<T>(val manager: Flow<Nothing>, val flows: Array<Flow<T>>)
* Note that this function requires the "manager" flow to be consumed alongside the split flows in
* order to function. Without this, all of the newly split flows will simply block.
*/
fun <T> Flow<T>.distribute(n: Int): DistributedFlow<T> {
internal fun <T> Flow<T>.distribute(n: Int): DistributedFlow<T> {
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
val managerFlow =
flow<Nothing> {

View file

@ -84,7 +84,7 @@ data class ImportedPlaylist(val name: String?, val paths: List<PossiblePaths>)
typealias PossiblePaths = List<Path>
class ExternalPlaylistManagerImpl(
private class ExternalPlaylistManagerImpl(
private val context: Context,
private val documentPathFactory: DocumentPathFactory,
private val m3u: M3U

View file

@ -28,7 +28,7 @@ package org.oxycblt.musikr.tag.format
* @return A list of one or more genre names, or null if this multi-value list has no valid
* formatting.
*/
fun List<String>.parseId3GenreNames() =
internal fun List<String>.parseId3GenreNames() =
if (size == 1) {
first().parseId3MultiValueGenre()
} else {

View file

@ -30,7 +30,7 @@ import org.oxycblt.musikr.util.positiveOrNull
*
* @see transformPositionField
*/
fun String.parseId3v2PositionField() =
internal fun String.parseId3v2PositionField() =
split('/', limit = 2).let {
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
}
@ -47,7 +47,7 @@ fun String.parseId3v2PositionField() =
*
* @see transformPositionField
*/
fun parseXiphPositionField(pos: String?, total: String?) =
internal fun parseXiphPositionField(pos: String?, total: String?) =
transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull())
/**
@ -59,7 +59,7 @@ fun parseXiphPositionField(pos: String?, total: String?) =
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*/
fun transformPositionField(pos: Int?, total: Int?) =
internal fun transformPositionField(pos: Int?, total: Int?) =
if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) {
pos
} else {

View file

@ -32,7 +32,7 @@ import org.oxycblt.musikr.tag.ReleaseType
import org.oxycblt.musikr.tag.ReplayGainAdjustment
import org.oxycblt.musikr.util.update
data class PreSong(
internal data class PreSong(
val musicBrainzId: UUID?,
val name: Name.Known,
val rawName: String,
@ -70,7 +70,7 @@ data class PreSong(
}
}
data class PreAlbum(
internal data class PreAlbum(
val musicBrainzId: UUID?,
val name: Name,
val rawName: String?,
@ -78,11 +78,11 @@ data class PreAlbum(
val preArtists: List<PreArtist>
)
data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?)
internal data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?)
data class PreGenre(
internal data class PreGenre(
val name: Name,
val rawName: String?,
)
data class PrePlaylist(val name: Name.Known, val rawName: String?, val handle: PlaylistHandle)
internal data class PrePlaylist(val name: Name.Known, val rawName: String?, val handle: PlaylistHandle)

View file

@ -30,7 +30,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.format.parseId3GenreNames
import org.oxycblt.musikr.util.toUuidOrNull
interface TagInterpreter {
internal interface TagInterpreter {
fun interpret(song: RawSong, interpretation: Interpretation): PreSong
companion object {

View file

@ -21,7 +21,9 @@ package org.oxycblt.musikr.tag.interpret
import java.text.CollationKey
/** An individual part of a name string that can be compared intelligently. */
data class Token(val collationKey: CollationKey, val type: Type) : Comparable<Token> {
data class Token(internal val collationKey: CollationKey, internal val type: Type) : Comparable<Token> {
val value: String get() = collationKey.sourceString
override fun compareTo(other: Token): Int {
// Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type)

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
* ExoPlayerTagFields.kt is part of Auxio.
* TagFields.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
@ -26,32 +26,32 @@ import org.oxycblt.musikr.tag.format.parseXiphPositionField
import org.oxycblt.musikr.util.nonZeroOrNull
// Song
fun Metadata.musicBrainzId() =
internal fun Metadata.musicBrainzId() =
(xiph["MUSICBRAINZ_RELEASETRACKID"]
?: xiph["MUSICBRAINZ RELEASE TRACK ID"]
?: id3v2["TXXX:MUSICBRAINZ RELEASE TRACK ID"]
?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"])
?.first()
fun Metadata.name() = (xiph["TITLE"] ?: id3v2["TIT2"])?.first()
internal fun Metadata.name() = (xiph["TITLE"] ?: id3v2["TIT2"])?.first()
fun Metadata.sortName() = (xiph["TITLESORT"] ?: id3v2["TSOT"])?.first()
internal fun Metadata.sortName() = (xiph["TITLESORT"] ?: id3v2["TSOT"])?.first()
// Track.
fun Metadata.track() =
internal fun Metadata.track() =
(parseXiphPositionField(
xiph["TRACKNUMBER"]?.first(),
(xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first())
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
// Disc and it's subtitle name.
fun Metadata.disc() =
internal fun Metadata.disc() =
(parseXiphPositionField(
xiph["DISCNUMBER"]?.first(),
(xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() })
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
// Dates are somewhat complicated, as not only did their semantics change from a flat year
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
@ -65,7 +65,7 @@ fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
// TODO: Show original and normal dates side-by-side
// TODO: Handle dates that are in "January" because the actual specific release date
// isn't known?
fun Metadata.date() =
internal fun Metadata.date() =
(xiph["ORIGINALDATE"]?.run { Date.from(first()) }
?: xiph["DATE"]?.run { Date.from(first()) }
?: xiph["YEAR"]?.run { Date.from(first()) }
@ -83,18 +83,18 @@ fun Metadata.date() =
?: parseId3v23Date())
// Album
fun Metadata.albumMusicBrainzId() =
internal fun Metadata.albumMusicBrainzId() =
(xiph["MUSICBRAINZ_ALBUMID"]
?: xiph["MUSICBRAINZ ALBUM ID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMID"])
?.first()
fun Metadata.albumName() = (xiph["ALBUM"] ?: id3v2["TALB"])?.first()
internal fun Metadata.albumName() = (xiph["ALBUM"] ?: id3v2["TALB"])?.first()
fun Metadata.albumSortName() = (xiph["ALBUMSORT"] ?: id3v2["TSOA"])?.first()
internal fun Metadata.albumSortName() = (xiph["ALBUMSORT"] ?: id3v2["TSOA"])?.first()
fun Metadata.releaseTypes() =
internal fun Metadata.releaseTypes() =
(xiph["RELEASETYPE"]
?: xiph["MUSICBRAINZ ALBUM TYPE"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM TYPE"]
@ -104,20 +104,20 @@ fun Metadata.releaseTypes() =
id3v2["GRP1"])
// Artist
fun Metadata.artistMusicBrainzIds() =
internal fun Metadata.artistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ARTISTID"]
?: xiph["MUSICBRAINZ ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ARTISTID"])
fun Metadata.artistNames() =
internal fun Metadata.artistNames() =
(xiph["ARTISTS"]
?: xiph["ARTIST"]
?: id3v2["TXXX:ARTISTS"]
?: id3v2["TPE1"]
?: id3v2["TXXX:ARTIST"])
fun Metadata.artistSortNames() =
internal fun Metadata.artistSortNames() =
(xiph["ARTISTSSORT"]
?: xiph["ARTISTS_SORT"]
?: xiph["ARTISTS SORT"]
@ -130,13 +130,13 @@ fun Metadata.artistSortNames() =
?: id3v2["TXXX:ARTISTSORT"]
?: id3v2["TXXX:ARTIST SORT"])
fun Metadata.albumArtistMusicBrainzIds() =
internal fun Metadata.albumArtistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ALBUMARTISTID"]
?: xiph["MUSICBRAINZ ALBUM ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMARTISTID"])
fun Metadata.albumArtistNames() =
internal fun Metadata.albumArtistNames() =
(xiph["ALBUMARTISTS"]
?: xiph["ALBUM_ARTISTS"]
?: xiph["ALBUM ARTISTS"]
@ -149,7 +149,7 @@ fun Metadata.albumArtistNames() =
?: id3v2["TXXX:ALBUMARTIST"]
?: id3v2["TXXX:ALBUM ARTIST"])
fun Metadata.albumArtistSortNames() =
internal fun Metadata.albumArtistSortNames() =
(xiph["ALBUMARTISTSSORT"]
?: xiph["ALBUMARTISTS_SORT"]
?: xiph["ALBUMARTISTS SORT"]
@ -164,10 +164,10 @@ fun Metadata.albumArtistSortNames() =
?: id3v2["TXXX:ALBUM ARTIST SORT"])
// Genre
fun Metadata.genreNames() = xiph["GENRE"] ?: id3v2["TCON"]
internal fun Metadata.genreNames() = xiph["GENRE"] ?: id3v2["TCON"]
// Compilation Flag
fun Metadata.isCompilation() =
internal fun Metadata.isCompilation() =
(xiph["COMPILATION"]
?: xiph["ITUNESCOMPILATION"]
?: id3v2["TCMP"] // This is a non-standard itunes extension
@ -179,16 +179,36 @@ fun Metadata.isCompilation() =
}
// ReplayGain information
fun Metadata.replayGainTrackAdjustment() =
internal fun Metadata.replayGainTrackAdjustment() =
(xiph["R128_TRACK_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment())
fun Metadata.replayGainAlbumAdjustment() =
internal fun Metadata.replayGainAlbumAdjustment() =
(xiph["R128_ALBUM_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment())
private fun List<String>.parseR128Adjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()?.run {
// Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale
this / 256f + 5
}
/**
* Parse a ReplayGain adjustment into a float value.
*
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
*/
private fun List<String>.parseReplayGainAdjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
/**
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla
*/
private val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
private fun Metadata.parseId3v23Date(): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
// is present.
@ -222,23 +242,3 @@ private fun Metadata.parseId3v23Date(): Date? {
return Date.from(year)
}
}
private fun List<String>.parseR128Adjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()?.run {
// Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale
this / 256f + 5
}
/**
* Parse a ReplayGain adjustment into a float value.
*
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
*/
private fun List<String>.parseReplayGainAdjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
/**
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla
*/
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }

View file

@ -18,10 +18,10 @@
package org.oxycblt.musikr.tag.parse
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
interface TagParser {
internal interface TagParser {
fun parse(file: DeviceFile, metadata: Metadata): ParsedTags
companion object {

View file

@ -29,7 +29,7 @@ import org.oxycblt.musikr.tag.Date
* otherwise, it aliases to the unchecked dereference operator (!!). This can be used as a minor
* optimization in certain cases.
*/
fun <T> unlikelyToBeNull(value: T?) =
internal fun <T> unlikelyToBeNull(value: T?) =
if (BuildConfig.DEBUG) {
requireNotNull(value)
} else {
@ -41,14 +41,14 @@ fun <T> unlikelyToBeNull(value: T?) =
*
* @return The given number if it's non-zero, null otherwise.
*/
fun Int.positiveOrNull() = if (this > 0) this else null
internal fun Int.positiveOrNull() = if (this > 0) this else null
/**
* Aliases a check to ensure that the given number is non-zero.
*
* @return The same number if it's non-zero, null otherwise.
*/
fun Float.nonZeroOrNull() = if (this != 0f) this else null
internal fun Float.nonZeroOrNull() = if (this != 0f) this else null
/**
* Aliases a check to ensure a given value is in a specified range.
@ -56,7 +56,7 @@ fun Float.nonZeroOrNull() = if (this != 0f) this else null
* @param range The valid range of values for this number.
* @return The same number if it is in the range, null otherwise.
*/
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
internal fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
/**
* Convert a [String] to a [UUID].
@ -64,7 +64,7 @@ fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else nul
* @return A [UUID] converted from the [String] value, or null if the value was not valid.
* @see UUID.fromString
*/
fun String.toUuidOrNull(): UUID? =
internal fun String.toUuidOrNull(): UUID? =
try {
UUID.fromString(this)
} catch (e: IllegalArgumentException) {
@ -76,7 +76,7 @@ fun String.toUuidOrNull(): UUID? =
*
* @param string The [String] to hash. If null, it will not be hashed.
*/
fun MessageDigest.update(string: String?) {
internal fun MessageDigest.update(string: String?) {
if (string != null) {
update(string.lowercase().toByteArray())
} else {
@ -89,7 +89,7 @@ fun MessageDigest.update(string: String?) {
*
* @param date The [Date] to hash. If null, nothing will be done.
*/
fun MessageDigest.update(date: Date?) {
internal fun MessageDigest.update(date: Date?) {
if (date != null) {
update(date.toString().toByteArray())
} else {
@ -102,7 +102,7 @@ fun MessageDigest.update(date: Date?) {
*
* @param strings The [String]s to hash. If a [String] is null, it will not be hashed.
*/
fun MessageDigest.update(strings: List<String?>) {
internal fun MessageDigest.update(strings: List<String?>) {
strings.forEach(::update)
}
@ -111,7 +111,7 @@ fun MessageDigest.update(strings: List<String?>) {
*
* @param n The [Int] to write. If null, nothing will be done.
*/
fun MessageDigest.update(n: Int?) {
internal fun MessageDigest.update(n: Int?) {
if (n != null) {
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
} else {
@ -126,7 +126,7 @@ fun MessageDigest.update(n: Int?) {
* @param clazz The [KClass] to reflect into.
* @param method The name of the method to obtain.
*/
fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = lazy {
internal fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = lazy {
clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also {
it.isAccessible = true
}

View file

@ -7,7 +7,7 @@ package org.oxycblt.musikr.util
* @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> {
internal inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
val split = mutableListOf<String>()
var currentString = ""
var i = 0
@ -53,11 +53,11 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<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 }
internal 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() }
internal fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }