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" 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 // Item used to be MusicType back when the music module was
// part of Auxio, so these old integer codes remain. // part of Auxio, so these old integer codes remain.
// TODO: Introduce new UID format that removes these. // TODO: Introduce new UID format that removes these.
@ -126,7 +126,7 @@ sealed interface Music {
@TypeConverter fun toMusicUid(string: String?) = string?.let(Companion::fromString) @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 * Creates an Auxio-style [UID] of random composition. Used if there is no
* non-subjective, unlikely-to-change metadata of the music. * non-subjective, unlikely-to-change metadata of the music.

View file

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

View file

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

View file

@ -31,7 +31,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover 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.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.cover
import java.security.MessageDigest import java.security.MessageDigest
interface CoverIdentifier { internal interface CoverIdentifier {
suspend fun identify(data: ByteArray): String suspend fun identify(data: ByteArray): String
companion object { 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, "audio/wave" to Wav,
) )
fun infer(containerMimeType: String, codecMimeType: String): Format { internal fun infer(containerMimeType: String, codecMimeType: String): Format {
val codecFormat = CODEC_MAP[codecMimeType] val codecFormat = CODEC_MAP[codecMimeType]
if (codecFormat != null) { if (codecFormat != null) {
// Codec found, possibly wrap in container. // 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.fs.query.contentResolverSafe
import org.oxycblt.musikr.util.splitEscaped 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 equals(other: Any?) = other is MusicLocation && uri == other.uri
override fun hashCode() = 31 * uri.hashCode() 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. * @param fileName The name of the file to append to the path.
* @return The new [Path] instance. * @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. * Resolves the [Path] in a human-readable format.
* *
* @param context [Context] required to obtain human-readable strings. * @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 { 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)) fun containing(other: Components) = Components(other.components.drop(components.size))
companion object { internal companion object {
fun nil() = Components(listOf())
/** /**
* Parses a path string into a [Components] instance by the unix path separator (/). * 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface DocumentPathFactory { internal interface DocumentPathFactory {
/** /**
* Unpacks a document URI into a [Path] instance, using [fromDocumentId]. * 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) * @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 * Extract a [Path] from the wrapped [Cursor]. This should be called after the cursor has been
* moved to the row that should be interpreted. * 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 * @see StorageManager.getStorageVolumes
*/ */
val StorageManager.storageVolumesCompat: List<StorageVolume> internal val StorageManager.storageVolumesCompat: List<StorageVolume>
get() = storageVolumes.toList() get() = storageVolumes.toList()
/** /**
@ -55,7 +55,7 @@ val StorageManager.storageVolumesCompat: List<StorageVolume>
* *
* @see StorageVolume.getDirectory * @see StorageVolume.getDirectory
*/ */
val StorageVolume.directoryCompat: String? internal val StorageVolume.directoryCompat: String?
@SuppressLint("NewApi") @SuppressLint("NewApi")
get() = get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 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. * @return A human-readable name for this volume.
*/ */
@SuppressLint("NewApi") @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 * 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 * @see StorageVolume.isPrimary
*/ */
val StorageVolume.isPrimaryCompat: Boolean internal val StorageVolume.isPrimaryCompat: Boolean
@SuppressLint("NewApi") get() = isPrimary @SuppressLint("NewApi") get() = isPrimary
/** /**
@ -93,14 +93,14 @@ val StorageVolume.isPrimaryCompat: Boolean
* *
* @see StorageVolume.isEmulated * @see StorageVolume.isEmulated
*/ */
val StorageVolume.isEmulatedCompat: Boolean internal val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi") get() = isEmulated @SuppressLint("NewApi") get() = isEmulated
/** /**
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary" * 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. * 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 // 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. // volumes use their UUID instead of primary in MediaStore/Document URIs.
get() = isPrimaryCompat && isEmulatedCompat get() = isPrimaryCompat && isEmulatedCompat
@ -111,7 +111,7 @@ val StorageVolume.isInternalCompat: Boolean
* *
* @see StorageVolume.getUuid * @see StorageVolume.getUuid
*/ */
val StorageVolume.uuidCompat: String? internal val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi") get() = uuid @SuppressLint("NewApi") get() = uuid
/** /**
@ -120,7 +120,7 @@ val StorageVolume.uuidCompat: String?
* *
* @see StorageVolume.getState * @see StorageVolume.getState
*/ */
val StorageVolume.stateCompat: String internal val StorageVolume.stateCompat: String
@SuppressLint("NewApi") get() = state @SuppressLint("NewApi") get() = state
/** /**
@ -129,7 +129,7 @@ val StorageVolume.stateCompat: String
* *
* @see StorageVolume.getMediaStoreVolumeName * @see StorageVolume.getMediaStoreVolumeName
*/ */
val StorageVolume.mediaStoreVolumeNameCompat: String? internal val StorageVolume.mediaStoreVolumeNameCompat: String?
get() = get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mediaStoreVolumeName mediaStoreVolumeName

View file

@ -25,7 +25,7 @@ import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Volume import org.oxycblt.musikr.fs.Volume
/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */ /** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
interface VolumeManager { internal interface VolumeManager {
/** /**
* The internal storage volume of the device. * 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.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
interface DeviceFiles { internal interface DeviceFiles {
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
companion object { 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) @OptIn(ExperimentalCoroutinesApi::class)
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> = override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
@ -99,7 +92,8 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De
mimeType, mimeType,
newPath, newPath,
size, size,
lastModified)) lastModified)
)
} }
} }
emitAll(recursive.asFlow().flattenMerge()) 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 * Get a content resolver that will not mangle MediaStore queries on certain devices. See
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info. * https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
*/ */
val Context.contentResolverSafe: ContentResolver internal val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver get() = applicationContext.contentResolver
/** /**
@ -42,7 +42,7 @@ val Context.contentResolverSafe: ContentResolver
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query * @see ContentResolver.query
*/ */
fun ContentResolver.safeQuery( internal fun ContentResolver.safeQuery(
uri: Uri, uri: Uri,
projection: Array<out String>, projection: Array<out String>,
selector: String? = null, selector: String? = null,
@ -63,7 +63,7 @@ fun ContentResolver.safeQuery(
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query * @see ContentResolver.query
*/ */
inline fun <reified R> ContentResolver.useQuery( internal inline fun <reified R> ContentResolver.useQuery(
uri: Uri, uri: Uri,
projection: Array<out String>, projection: Array<out String>,
selector: String? = null, 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.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull
data class MusicGraph( internal data class MusicGraph(
val songVertex: List<SongVertex>, val songVertex: List<SongVertex>,
val albumVertex: List<AlbumVertex>, val albumVertex: List<AlbumVertex>,
val artistVertex: List<ArtistVertex>, val artistVertex: List<ArtistVertex>,
@ -274,7 +274,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
} }
} }
class SongVertex( internal class SongVertex(
val preSong: PreSong, val preSong: PreSong,
var albumVertex: AlbumVertex, var albumVertex: AlbumVertex,
var artistVertices: MutableList<ArtistVertex>, var artistVertices: MutableList<ArtistVertex>,
@ -283,12 +283,12 @@ class SongVertex(
var tag: Any? = null 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>() val songVertices = mutableSetOf<SongVertex>()
var tag: Any? = null var tag: Any? = null
} }
class ArtistVertex( internal class ArtistVertex(
val preArtist: PreArtist, val preArtist: PreArtist,
) { ) {
val songVertices = mutableSetOf<SongVertex>() val songVertices = mutableSetOf<SongVertex>()
@ -297,7 +297,7 @@ class ArtistVertex(
var tag: Any? = null var tag: Any? = null
} }
class GenreVertex(val preGenre: PreGenre) { internal class GenreVertex(val preGenre: PreGenre) {
val songVertices = mutableSetOf<SongVertex>() val songVertices = mutableSetOf<SongVertex>()
val artistVertices = mutableSetOf<ArtistVertex>() val artistVertices = mutableSetOf<ArtistVertex>()
var tag: Any? = null var tag: Any? = null

View file

@ -22,7 +22,7 @@ import android.content.Context
import java.io.FileInputStream import java.io.FileInputStream
import java.nio.ByteBuffer 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 fileName = fileRef.fileName
private val fd = private val fd =
requireNotNull(context.contentResolver.openFileDescriptor(fileRef.uri, "r")) { 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 android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull
interface MetadataExtractor { internal interface MetadataExtractor {
suspend fun extract(file: DeviceFile): Metadata? suspend fun extract(file: DeviceFile): Metadata?
companion object { 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. * 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 name(): String
fun readBlock(length: Long): ByteArray fun readBlock(length: Long): ByteArray

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
object TagLibJNI { internal object TagLibJNI {
init { init {
System.loadLibrary("taglib_jni") System.loadLibrary("taglib_jni")
} }
@ -41,46 +41,4 @@ object TagLibJNI {
private external fun openNative(ioStream: AndroidInputStream): Metadata? private external fun openNative(ioStream: AndroidInputStream): Metadata?
} }
data class FileRef(val fileName: String, val uri: Uri) internal 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,
)

View file

@ -27,7 +27,7 @@ import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
interface AlbumCore { internal interface AlbumCore {
val preAlbum: PreAlbum val preAlbum: PreAlbum
val songs: List<Song> val songs: List<Song>
@ -39,7 +39,7 @@ interface AlbumCore {
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumImpl(private val core: AlbumCore) : Album { internal class AlbumImpl(private val core: AlbumCore) : Album {
private val preAlbum = core.preAlbum private val preAlbum = core.preAlbum
override val uid = 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.tag.interpret.PreArtist
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
interface ArtistCore { internal interface ArtistCore {
val preArtist: PreArtist val preArtist: PreArtist
val songs: Set<Song> val songs: Set<Song>
val albums: Set<Album> val albums: Set<Album>
@ -40,7 +40,7 @@ interface ArtistCore {
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistImpl(private val core: ArtistCore) : Artist { internal class ArtistImpl(private val core: ArtistCore) : Artist {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed 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) } 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.tag.interpret.PreGenre
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
interface GenreCore { internal interface GenreCore {
val preGenre: PreGenre val preGenre: PreGenre
val songs: Set<Song> val songs: Set<Song>
val artists: Set<Artist> val artists: Set<Artist>
@ -37,7 +37,7 @@ interface GenreCore {
* *
* @author Alexander Capehart (OxygenCobalt) * @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 uid = Music.UID.auxio(Music.UID.Item.GENRE) { update(core.preGenre.rawName) }
override val name = core.preGenre.name 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.MusicGraph
import org.oxycblt.musikr.graph.SongVertex import org.oxycblt.musikr.graph.SongVertex
interface LibraryFactory { internal interface LibraryFactory {
fun create(graph: MusicGraph): MutableLibrary fun create(graph: MusicGraph): MutableLibrary
companion object { companion object {

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
class LibraryImpl( internal class LibraryImpl(
override val songs: Collection<SongImpl>, override val songs: Collection<SongImpl>,
override val albums: Collection<AlbumImpl>, override val albums: Collection<AlbumImpl>,
override val artists: Collection<ArtistImpl>, 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.Name
import org.oxycblt.musikr.tag.interpret.PrePlaylist import org.oxycblt.musikr.tag.interpret.PrePlaylist
interface PlaylistCore { internal interface PlaylistCore {
val prePlaylist: PrePlaylist val prePlaylist: PrePlaylist
val handle: PlaylistHandle val handle: PlaylistHandle
val songs: List<Song> 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 uid = core.handle.uid
override val name: Name.Known = core.prePlaylist.name override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs } 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.Song
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
interface SongCore { internal interface SongCore {
val preSong: PreSong val preSong: PreSong
fun resolveAlbum(): Album fun resolveAlbum(): Album
@ -39,7 +39,7 @@ interface SongCore {
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SongImpl(private val handle: SongCore) : Song { internal class SongImpl(private val handle: SongCore) : Song {
private val preSong = handle.preSong private val preSong = handle.preSong
override val uid = preSong.computeUid() 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.model.LibraryFactory
import org.oxycblt.musikr.tag.interpret.TagInterpreter import org.oxycblt.musikr.tag.interpret.TagInterpreter
interface EvaluateStep { internal interface EvaluateStep {
suspend fun evaluate( suspend fun evaluate(
interpretation: Interpretation, interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic> extractedMusic: Flow<ExtractedMusic>

View file

@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import org.oxycblt.musikr.fs.MusicLocation 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.fs.query.DeviceFiles
import org.oxycblt.musikr.playlist.m3u.M3U import org.oxycblt.musikr.playlist.m3u.M3U
interface ExploreStep { internal interface ExploreStep {
fun explore(locations: List<MusicLocation>): Flow<ExploreNode> fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
companion object { companion object {
@ -51,6 +51,6 @@ private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreSte
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
} }
sealed interface ExploreNode { internal sealed interface ExploreNode {
data class Audio(val file: DeviceFile) : 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.Storage
import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser import org.oxycblt.musikr.tag.parse.TagParser
interface ExtractStep { internal interface ExtractStep {
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
companion object { companion object {
@ -99,6 +99,6 @@ data class RawSong(
val cover: Cover.Single? val cover: Cover.Single?
) )
sealed interface ExtractedMusic { internal sealed interface ExtractedMusic {
data class Song(val song: RawSong) : 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.receiveAsFlow
import kotlinx.coroutines.flow.withIndex 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 Left<L, R>(val value: L) : Divert<L, R>
data class Right<L, R>(val value: R) : 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>) 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> crossinline predicate: (T) -> Divert<L, R>
): DivertedFlow<L, R> { ): DivertedFlow<L, R> {
val leftChannel = Channel<L>(Channel.UNLIMITED) 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()) 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. * 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 * Note that this function requires the "manager" flow to be consumed alongside the split flows in
* order to function. Without this, all of the newly split flows will simply block. * order to function. Without this, all of the newly split flows will simply block.
*/ */
fun <T> Flow<T>.distribute(n: Int): DistributedFlow<T> { internal fun <T> Flow<T>.distribute(n: Int): DistributedFlow<T> {
val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) } val posChannels = Array(n) { Channel<T>(Channel.UNLIMITED) }
val managerFlow = val managerFlow =
flow<Nothing> { flow<Nothing> {

View file

@ -84,7 +84,7 @@ data class ImportedPlaylist(val name: String?, val paths: List<PossiblePaths>)
typealias PossiblePaths = List<Path> typealias PossiblePaths = List<Path>
class ExternalPlaylistManagerImpl( private class ExternalPlaylistManagerImpl(
private val context: Context, private val context: Context,
private val documentPathFactory: DocumentPathFactory, private val documentPathFactory: DocumentPathFactory,
private val m3u: M3U 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 * @return A list of one or more genre names, or null if this multi-value list has no valid
* formatting. * formatting.
*/ */
fun List<String>.parseId3GenreNames() = internal fun List<String>.parseId3GenreNames() =
if (size == 1) { if (size == 1) {
first().parseId3MultiValueGenre() first().parseId3MultiValueGenre()
} else { } else {

View file

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

View file

@ -32,7 +32,7 @@ import org.oxycblt.musikr.tag.ReleaseType
import org.oxycblt.musikr.tag.ReplayGainAdjustment import org.oxycblt.musikr.tag.ReplayGainAdjustment
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
data class PreSong( internal data class PreSong(
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
val name: Name.Known, val name: Name.Known,
val rawName: String, val rawName: String,
@ -70,7 +70,7 @@ data class PreSong(
} }
} }
data class PreAlbum( internal data class PreAlbum(
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
val name: Name, val name: Name,
val rawName: String?, val rawName: String?,
@ -78,11 +78,11 @@ data class PreAlbum(
val preArtists: List<PreArtist> 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 name: Name,
val rawName: String?, 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.tag.format.parseId3GenreNames
import org.oxycblt.musikr.util.toUuidOrNull import org.oxycblt.musikr.util.toUuidOrNull
interface TagInterpreter { internal interface TagInterpreter {
fun interpret(song: RawSong, interpretation: Interpretation): PreSong fun interpret(song: RawSong, interpretation: Interpretation): PreSong
companion object { companion object {

View file

@ -21,7 +21,9 @@ package org.oxycblt.musikr.tag.interpret
import java.text.CollationKey import java.text.CollationKey
/** An individual part of a name string that can be compared intelligently. */ /** 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 { override fun compareTo(other: Token): Int {
// Numeric tokens should always be lower than lexicographic tokens. // Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type) val modeComp = type.compareTo(other.type)

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * 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 * 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
@ -26,32 +26,32 @@ import org.oxycblt.musikr.tag.format.parseXiphPositionField
import org.oxycblt.musikr.util.nonZeroOrNull import org.oxycblt.musikr.util.nonZeroOrNull
// Song // Song
fun Metadata.musicBrainzId() = internal fun Metadata.musicBrainzId() =
(xiph["MUSICBRAINZ_RELEASETRACKID"] (xiph["MUSICBRAINZ_RELEASETRACKID"]
?: xiph["MUSICBRAINZ RELEASE TRACK ID"] ?: xiph["MUSICBRAINZ RELEASE TRACK ID"]
?: id3v2["TXXX:MUSICBRAINZ RELEASE TRACK ID"] ?: id3v2["TXXX:MUSICBRAINZ RELEASE TRACK ID"]
?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"]) ?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"])
?.first() ?.first()
fun 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. // Track.
fun Metadata.track() = internal fun Metadata.track() =
(parseXiphPositionField( (parseXiphPositionField(
xiph["TRACKNUMBER"]?.first(), xiph["TRACKNUMBER"]?.first(),
(xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first()) (xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first())
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() }) ?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
// Disc and it's subtitle name. // Disc and it's subtitle name.
fun Metadata.disc() = internal fun Metadata.disc() =
(parseXiphPositionField( (parseXiphPositionField(
xiph["DISCNUMBER"]?.first(), xiph["DISCNUMBER"]?.first(),
(xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() }) (xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() })
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() }) ?: 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 // Dates are somewhat complicated, as not only did their semantics change from a flat year
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of // value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
@ -65,7 +65,7 @@ fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
// TODO: Show original and normal dates side-by-side // TODO: Show original and normal dates side-by-side
// TODO: Handle dates that are in "January" because the actual specific release date // TODO: Handle dates that are in "January" because the actual specific release date
// isn't known? // isn't known?
fun Metadata.date() = internal fun Metadata.date() =
(xiph["ORIGINALDATE"]?.run { Date.from(first()) } (xiph["ORIGINALDATE"]?.run { Date.from(first()) }
?: xiph["DATE"]?.run { Date.from(first()) } ?: xiph["DATE"]?.run { Date.from(first()) }
?: xiph["YEAR"]?.run { Date.from(first()) } ?: xiph["YEAR"]?.run { Date.from(first()) }
@ -83,18 +83,18 @@ fun Metadata.date() =
?: parseId3v23Date()) ?: parseId3v23Date())
// Album // Album
fun Metadata.albumMusicBrainzId() = internal fun Metadata.albumMusicBrainzId() =
(xiph["MUSICBRAINZ_ALBUMID"] (xiph["MUSICBRAINZ_ALBUMID"]
?: xiph["MUSICBRAINZ ALBUM ID"] ?: xiph["MUSICBRAINZ ALBUM ID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ID"] ?: id3v2["TXXX:MUSICBRAINZ ALBUM ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMID"]) ?: id3v2["TXXX:MUSICBRAINZ_ALBUMID"])
?.first() ?.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["RELEASETYPE"]
?: xiph["MUSICBRAINZ ALBUM TYPE"] ?: xiph["MUSICBRAINZ ALBUM TYPE"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM TYPE"] ?: id3v2["TXXX:MUSICBRAINZ ALBUM TYPE"]
@ -104,20 +104,20 @@ fun Metadata.releaseTypes() =
id3v2["GRP1"]) id3v2["GRP1"])
// Artist // Artist
fun Metadata.artistMusicBrainzIds() = internal fun Metadata.artistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ARTISTID"] (xiph["MUSICBRAINZ_ARTISTID"]
?: xiph["MUSICBRAINZ ARTIST ID"] ?: xiph["MUSICBRAINZ ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ ARTIST ID"] ?: id3v2["TXXX:MUSICBRAINZ ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ARTISTID"]) ?: id3v2["TXXX:MUSICBRAINZ_ARTISTID"])
fun Metadata.artistNames() = internal fun Metadata.artistNames() =
(xiph["ARTISTS"] (xiph["ARTISTS"]
?: xiph["ARTIST"] ?: xiph["ARTIST"]
?: id3v2["TXXX:ARTISTS"] ?: id3v2["TXXX:ARTISTS"]
?: id3v2["TPE1"] ?: id3v2["TPE1"]
?: id3v2["TXXX:ARTIST"]) ?: id3v2["TXXX:ARTIST"])
fun Metadata.artistSortNames() = internal fun Metadata.artistSortNames() =
(xiph["ARTISTSSORT"] (xiph["ARTISTSSORT"]
?: xiph["ARTISTS_SORT"] ?: xiph["ARTISTS_SORT"]
?: xiph["ARTISTS SORT"] ?: xiph["ARTISTS SORT"]
@ -130,13 +130,13 @@ fun Metadata.artistSortNames() =
?: id3v2["TXXX:ARTISTSORT"] ?: id3v2["TXXX:ARTISTSORT"]
?: id3v2["TXXX:ARTIST SORT"]) ?: id3v2["TXXX:ARTIST SORT"])
fun Metadata.albumArtistMusicBrainzIds() = internal fun Metadata.albumArtistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ALBUMARTISTID"] (xiph["MUSICBRAINZ_ALBUMARTISTID"]
?: xiph["MUSICBRAINZ ALBUM ARTIST ID"] ?: xiph["MUSICBRAINZ ALBUM ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ARTIST ID"] ?: id3v2["TXXX:MUSICBRAINZ ALBUM ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMARTISTID"]) ?: id3v2["TXXX:MUSICBRAINZ_ALBUMARTISTID"])
fun Metadata.albumArtistNames() = internal fun Metadata.albumArtistNames() =
(xiph["ALBUMARTISTS"] (xiph["ALBUMARTISTS"]
?: xiph["ALBUM_ARTISTS"] ?: xiph["ALBUM_ARTISTS"]
?: xiph["ALBUM ARTISTS"] ?: xiph["ALBUM ARTISTS"]
@ -149,7 +149,7 @@ fun Metadata.albumArtistNames() =
?: id3v2["TXXX:ALBUMARTIST"] ?: id3v2["TXXX:ALBUMARTIST"]
?: id3v2["TXXX:ALBUM ARTIST"]) ?: id3v2["TXXX:ALBUM ARTIST"])
fun Metadata.albumArtistSortNames() = internal fun Metadata.albumArtistSortNames() =
(xiph["ALBUMARTISTSSORT"] (xiph["ALBUMARTISTSSORT"]
?: xiph["ALBUMARTISTS_SORT"] ?: xiph["ALBUMARTISTS_SORT"]
?: xiph["ALBUMARTISTS SORT"] ?: xiph["ALBUMARTISTS SORT"]
@ -164,10 +164,10 @@ fun Metadata.albumArtistSortNames() =
?: id3v2["TXXX:ALBUM ARTIST SORT"]) ?: id3v2["TXXX:ALBUM ARTIST SORT"])
// Genre // Genre
fun Metadata.genreNames() = xiph["GENRE"] ?: id3v2["TCON"] internal fun Metadata.genreNames() = xiph["GENRE"] ?: id3v2["TCON"]
// Compilation Flag // Compilation Flag
fun Metadata.isCompilation() = internal fun Metadata.isCompilation() =
(xiph["COMPILATION"] (xiph["COMPILATION"]
?: xiph["ITUNESCOMPILATION"] ?: xiph["ITUNESCOMPILATION"]
?: id3v2["TCMP"] // This is a non-standard itunes extension ?: id3v2["TCMP"] // This is a non-standard itunes extension
@ -179,16 +179,36 @@ fun Metadata.isCompilation() =
} }
// ReplayGain information // ReplayGain information
fun Metadata.replayGainTrackAdjustment() = internal fun Metadata.replayGainTrackAdjustment() =
(xiph["R128_TRACK_GAIN"]?.parseR128Adjustment() (xiph["R128_TRACK_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment() ?: xiph["REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()) ?: id3v2["TXXX:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment())
fun Metadata.replayGainAlbumAdjustment() = internal fun Metadata.replayGainAlbumAdjustment() =
(xiph["R128_ALBUM_GAIN"]?.parseR128Adjustment() (xiph["R128_ALBUM_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment() ?: xiph["REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX: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? { private fun Metadata.parseId3v23Date(): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
// is present. // is present.
@ -222,23 +242,3 @@ private fun Metadata.parseId3v23Date(): Date? {
return Date.from(year) 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 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 import org.oxycblt.musikr.metadata.Metadata
interface TagParser { internal interface TagParser {
fun parse(file: DeviceFile, metadata: Metadata): ParsedTags fun parse(file: DeviceFile, metadata: Metadata): ParsedTags
companion object { 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 * otherwise, it aliases to the unchecked dereference operator (!!). This can be used as a minor
* optimization in certain cases. * optimization in certain cases.
*/ */
fun <T> unlikelyToBeNull(value: T?) = internal fun <T> unlikelyToBeNull(value: T?) =
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
requireNotNull(value) requireNotNull(value)
} else { } else {
@ -41,14 +41,14 @@ fun <T> unlikelyToBeNull(value: T?) =
* *
* @return The given number if it's non-zero, null otherwise. * @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. * Aliases a check to ensure that the given number is non-zero.
* *
* @return The same number if it's non-zero, null otherwise. * @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. * 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. * @param range The valid range of values for this number.
* @return The same number if it is in the range, null otherwise. * @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]. * 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. * @return A [UUID] converted from the [String] value, or null if the value was not valid.
* @see UUID.fromString * @see UUID.fromString
*/ */
fun String.toUuidOrNull(): UUID? = internal fun String.toUuidOrNull(): UUID? =
try { try {
UUID.fromString(this) UUID.fromString(this)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@ -76,7 +76,7 @@ fun String.toUuidOrNull(): UUID? =
* *
* @param string The [String] to hash. If null, it will not be hashed. * @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) { if (string != null) {
update(string.lowercase().toByteArray()) update(string.lowercase().toByteArray())
} else { } else {
@ -89,7 +89,7 @@ fun MessageDigest.update(string: String?) {
* *
* @param date The [Date] to hash. If null, nothing will be done. * @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) { if (date != null) {
update(date.toString().toByteArray()) update(date.toString().toByteArray())
} else { } 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. * @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) 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. * @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) { if (n != null) {
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte())) update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
} else { } else {
@ -126,7 +126,7 @@ fun MessageDigest.update(n: Int?) {
* @param clazz The [KClass] to reflect into. * @param clazz The [KClass] to reflect into.
* @param method The name of the method to obtain. * @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 { clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also {
it.isAccessible = true 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. * @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. * @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>() val split = mutableListOf<String>()
var currentString = "" var currentString = ""
var i = 0 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 * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or
* empty. * 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. * Fix trailing whitespace or blank contents within a list of [String]s.
* *
* @return A list of non-blank strings with trailing whitespace removed. * @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() }