musikr: cleanup api
This commit is contained in:
parent
14355a1005
commit
0d5abb6407
42 changed files with 189 additions and 183 deletions
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
11
musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
Normal file
11
musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
Normal 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
|
||||||
|
)
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 (/).
|
||||||
*
|
*
|
||||||
|
|
|
@ -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].
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")) {
|
||||||
|
|
43
musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
Normal file
43
musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
Normal 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,
|
||||||
|
)
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.-]") }
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
Loading…
Reference in a new issue