all: break off musikr

This commit is contained in:
Alexander Capehart 2024-12-16 13:09:08 -05:00
parent f33377cf26
commit e908d0e102
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
84 changed files with 151 additions and 286 deletions

5
.gitmodules vendored
View file

@ -1,7 +1,8 @@
[submodule "media"]
path = media
url = https://github.com/OxygenCobalt/media.git
[submodule "taglib"]
path = ktaglib/src/main/cpp/taglib
[submodule "musikr/src/main/cpp/taglib"]
path = musikr/src/main/cpp/taglib
url = https://github.com/taglib/taglib.git
tag = v2.0.2

View file

@ -2,7 +2,6 @@ plugins {
id "com.android.application"
id "kotlin-android"
id "androidx.navigation.safeargs.kotlin"
id "com.diffplug.spotless"
id "kotlin-parcelize"
id "dagger.hilt.android.plugin"
id "kotlin-kapt"
@ -12,11 +11,9 @@ plugins {
android {
compileSdk 35
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify
// it here so that binary stripping will work.
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
// NDK use is unified
ndkVersion "26.3.11579264"
// Auxio implicitly depends on the native modules, explicitly specify it
// here so the libraries are still stripped.
ndkVersion ndk_version
namespace "org.oxycblt.auxio"
defaultConfig {
@ -24,8 +21,8 @@ android {
versionName "3.6.3"
versionCode 53
minSdk 24
targetSdk 35
minSdk target_sdk
targetSdk target_sdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -80,9 +77,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
def coroutines_version = '1.7.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlin_coroutines_version"
// --- SUPPORT ---
@ -125,20 +121,23 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.2.1"
// Database
def room_version = '2.6.1'
implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Build
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.3"
// --- SECOND PARTY ---
// Musikr
implementation project(":musikr")
// --- THIRD PARTY ---
// Exoplayer (Vendored)
implementation project(":media-lib-exoplayer")
implementation project(":media-lib-decoder-ffmpeg")
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.3"
// Taglib
implementation project(":ktaglib")
// Image loading
implementation 'io.coil-kt.coil3:coil-core:3.0.2'
@ -175,15 +174,3 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
spotless {
kotlin {
target "src/**/*.kt"
ktfmt().dropboxStyle()
licenseHeaderFile("NOTICE")
}
}
afterEvaluate {
preDebugBuild.dependsOn spotlessApply
}

View file

@ -54,7 +54,6 @@ import org.oxycblt.musikr.Music
import org.oxycblt.musikr.MusicParent
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.metadata.AudioProperties
import timber.log.Timber as L
/**
@ -69,7 +68,6 @@ class DetailViewModel
constructor(
private val listSettings: ListSettings,
private val musicRepository: MusicRepository,
private val audioPropertiesFactory: AudioProperties.Factory,
private val playbackSettings: PlaybackSettings,
detailGeneratorFactory: DetailGenerator.Factory
) : ViewModel(), DetailGenerator.Invalidator {
@ -89,10 +87,6 @@ constructor(
val currentSong: StateFlow<Song?>
get() = _currentSong
private val _songAudioProperties = MutableStateFlow<AudioProperties?>(null)
/** The [AudioProperties] of the currently shown [Song]. Null if not loaded yet. */
val songAudioProperties: StateFlow<AudioProperties?> = _songAudioProperties
// --- ALBUM ---
private val _currentAlbum = MutableStateFlow<Album?>(null)
@ -308,7 +302,7 @@ constructor(
}
/**
* Set a new [currentSong] from it's [Music.UID]. [currentSong] and [songAudioProperties] will
* Set a new [currentSong] from it's [Music.UID]. [currentSong] will
* be updated to align with the new [Song].
*
* @param uid The UID of the [Song] to load. Must be valid.
@ -511,17 +505,7 @@ constructor(
}
private fun refreshAudioInfo(song: Song) {
L.d("Refreshing audio info")
// Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel()
_songAudioProperties.value = null
currentSongJob =
viewModelScope.launch(Dispatchers.IO) {
val info = audioPropertiesFactory.extract(song)
yield()
L.d("Updating audio info to $info")
_songAudioProperties.value = info
}
}
private inline fun <T : MusicParent> refreshDetail(

View file

@ -41,7 +41,6 @@ import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.concatLocalized
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.metadata.AudioProperties
import org.oxycblt.musikr.tag.Name
import timber.log.Timber as L
@ -72,63 +71,63 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setSong(args.songUid)
detailModel.toShow.consume()
collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong)
collectImmediately(detailModel.currentSong, ::updateSong)
}
private fun updateSong(song: Song?, info: AudioProperties?) {
if (song == null) {
private fun updateSong(song: Song?) {
// if (song == null) {
L.d("No song to show, navigating away")
findNavController().navigateUp()
return
}
if (info != null) {
val context = requireContext()
detailAdapter.update(
buildList {
add(SongProperty(R.string.lbl_name, song.zipName(context)))
add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
add(SongProperty(R.string.lbl_artists, song.artists.zipNames(context)))
add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context)))
song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) }
song.track?.let {
add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
}
song.disc?.let {
val formattedNumber = getString(R.string.fmt_number, it.number)
val zipped =
if (it.name != null) {
getString(R.string.fmt_zipped_names, formattedNumber, it.name)
} else {
formattedNumber
}
add(SongProperty(R.string.lbl_disc, zipped))
}
add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
// info.format.resolveName(context)?.let {
// add(SongProperty(R.string.lbl_format, it))
// }
add(
SongProperty(
R.string.lbl_size, Formatter.formatFileSize(context, song.size)))
add(SongProperty(R.string.lbl_duration, song.durationMs.formatDurationMs(true)))
info.bitrateKbps?.let {
add(SongProperty(R.string.lbl_bitrate, getString(R.string.fmt_bitrate, it)))
}
info.sampleRateHz?.let {
add(
SongProperty(
R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
}
song.replayGainAdjustment.track?.let {
add(SongProperty(R.string.lbl_replaygain_track, it.formatDb(context)))
}
song.replayGainAdjustment.album?.let {
add(SongProperty(R.string.lbl_replaygain_album, it.formatDb(context)))
}
},
UpdateInstructions.Replace(0))
}
// }
//
// if (info != null) {
// val context = requireContext()
// detailAdapter.update(
// buildList {
// add(SongProperty(R.string.lbl_name, song.zipName(context)))
// add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
// add(SongProperty(R.string.lbl_artists, song.artists.zipNames(context)))
// add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context)))
// song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) }
// song.track?.let {
// add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
// }
// song.disc?.let {
// val formattedNumber = getString(R.string.fmt_number, it.number)
// val zipped =
// if (it.name != null) {
// getString(R.string.fmt_zipped_names, formattedNumber, it.name)
// } else {
// formattedNumber
// }
// add(SongProperty(R.string.lbl_disc, zipped))
// }
// add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
// // info.format.resolveName(context)?.let {
// // add(SongProperty(R.string.lbl_format, it))
// // }
// add(
// SongProperty(
// R.string.lbl_size, Formatter.formatFileSize(context, song.size)))
// add(SongProperty(R.string.lbl_duration, song.durationMs.formatDurationMs(true)))
// info.bitrateKbps?.let {
// add(SongProperty(R.string.lbl_bitrate, getString(R.string.fmt_bitrate, it)))
// }
// info.sampleRateHz?.let {
// add(
// SongProperty(
// R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
// }
// song.replayGainAdjustment.track?.let {
// add(SongProperty(R.string.lbl_replaygain_track, it.formatDb(context)))
// }
// song.replayGainAdjustment.album?.let {
// add(SongProperty(R.string.lbl_replaygain_album, it.formatDb(context)))
// }
// },
// UpdateInstructions.Replace(0))
// }
}
private fun <T : Music> T.zipName(context: Context): String {

View file

@ -1,105 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* AudioProperties.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata
import android.content.Context
import android.media.MediaExtractor
import android.media.MediaFormat
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.musikr.Song
import timber.log.Timber as L
/**
* The properties of a [Song]'s file.
*
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz.
* @author Alexander Capehart (OxygenCobalt)
*/
data class AudioProperties(val bitrateKbps: Int?, val sampleRateHz: Int?) {
/** Implements the process of extracting [AudioProperties] from a given [Song]. */
interface Factory {
/**
* Extract the [AudioProperties] of a given [Song].
*
* @param song The [Song] to read.
* @return The [AudioProperties] of the [Song], if possible to obtain.
*/
suspend fun extract(song: Song): AudioProperties
}
}
/**
* A framework-backed implementation of [AudioProperties.Factory].
*
* @param context [Context] required to read audio files.
*/
class AudioPropertiesFactoryImpl
@Inject
constructor(@ApplicationContext private val context: Context) : AudioProperties.Factory {
override suspend fun extract(song: Song): AudioProperties {
// While we would use ExoPlayer to extract this information, it doesn't support
// common data like bit rate in progressive data sources due to there being no
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
val extractor = MediaExtractor()
try {
extractor.setDataSource(context, song.uri, emptyMap())
} catch (e: Exception) {
// Can feasibly fail with invalid file formats. Note that this isn't considered
// an error condition in the UI, as there is still plenty of other song information
// that we can show.
L.w("Unable to extract song attributes.")
L.w(e.stackTraceToString())
return AudioProperties(null, null)
}
// Get the first track from the extractor (This is basically always the only
// track we need to analyze).
val format = extractor.getTrackFormat(0)
// Accessing fields can throw an exception if the fields are not present, and
// the new method for using default values is not available on lower API levels.
// So, we are forced to handle the exception and map it to a saner null value.
val bitrate =
try {
// Convert bytes-per-second to kilobytes-per-second.
format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000
} catch (e: NullPointerException) {
L.d("Unable to extract bit rate field")
null
}
val sampleRate =
try {
format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
} catch (e: NullPointerException) {
L.e("Unable to extract sample rate field")
null
}
extractor.release()
L.d("Finished extracting audio properties")
return AudioProperties(bitrate, sampleRate)
}
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* MetadataModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface MetadataModule {
@Binds
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
}

View file

@ -1,8 +1,16 @@
buildscript {
ext {
agp_version = '8.7.3'
kotlin_version = '2.0.21'
kotlin_coroutines_version = '1.7.2'
navigation_version = "2.8.3"
hilt_version = '2.51.1'
room_version = '2.6.1'
min_sdk = 24
target_sdk = 35
ndk_version = "27.2.12479018"
}
dependencies {
@ -12,14 +20,27 @@ buildscript {
}
plugins {
id "com.android.application" version '8.7.2' apply false
// Android studio doesn't understand this syntax
//noinspection GradlePluginVersion
id "com.android.application" version "$agp_version" apply false
id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false
//noinspection GradlePluginVersion
id 'com.android.library' version "$agp_version" apply false
id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false
id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false
id "com.diffplug.spotless" version "6.25.0" apply false
id 'com.android.library' version '8.7.2' apply false
id "com.diffplug.spotless" version "6.25.0" apply true
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
spotless {
kotlin {
target "*/src/**/*.kt"
ktfmt().dropboxStyle()
licenseHeaderFile("NOTICE")
}
cpp {
target "*/src/**/*.cpp"
clangFormat()
licenseHeaderFile("NOTICE")
}
}

View file

@ -3,15 +3,19 @@ import org.apache.tools.ant.taskdefs.condition.Os
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id "com.google.devtools.ksp"
id "com.diffplug.spotless"
id "kotlin-parcelize"
}
android {
namespace 'org.oxycblt.ktaglib'
compileSdk 34
ndkVersion "26.3.11579264"
namespace 'org.oxycblt.musikr'
compileSdk target_sdk
ndkVersion "$ndk_version"
defaultConfig {
minSdk 24
minSdk min_sdk
targetSdk target_sdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@ -28,19 +32,45 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = "17"
freeCompilerArgs += "-Xjvm-default=all"
}
buildFeatures {
buildConfig true
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
// AndroidX
implementation "androidx.core:core-ktx:1.15.0"
// Database
implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Build
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.3"
}
task assembleTaglib(type: Exec) {

View file

View file

@ -80,7 +80,7 @@ public:
/*!
* Reset the end-of-stream and error flags on the stream.
*/
void clear();
void clear() override;
/*!
* Returns the current offset within the stream.

View file

@ -27,14 +27,16 @@ build_for_arch() {
cd $TAGLIB_SRC_DIR
cmake -B $DST_DIR -DANDROID_NDK_PATH=${NDK_PATH} -DCMAKE_TOOLCHAIN_FILE=${NDK_TOOLCHAIN} \
-DANDROID_ABI=$ARCH -DBUILD_SHARED_LIBS=OFF -DVISIBILITY_HIDDEN=ON -DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DWITH_ZLIB=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build $DST_DIR --config Release
-DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DWITH_ZLIB=OFF -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS="-fPIC"
cmake --build $DST_DIR --config Release -j$(nproc)
cd $WORKING_DIR
cmake --install $DST_DIR --config Release --prefix $PKG_DIR --strip
}
build_for_arch $X86_ARCH
build_for_arch $X86_64_ARCH
build_for_arch $ARMV7_ARCH
build_for_arch $ARMV8_ARCH
build_for_arch $X86_ARCH&
build_for_arch $X86_64_ARCH&
build_for_arch $ARMV7_ARCH&
build_for_arch $ARMV8_ARCH&
wait

View file

@ -22,7 +22,6 @@ import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
@ -78,7 +77,7 @@ interface DocumentPathFactory {
}
private class DocumentPathFactoryImpl(
@ApplicationContext private val context: Context,
private val context: Context,
private val volumeManager: VolumeManager,
private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory
) : DocumentPathFactory {

View file

@ -23,7 +23,6 @@ import android.os.Build
import android.provider.MediaStore
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import timber.log.Timber as L
/**
* Wrapper around a [Cursor] that interprets path information on a per-API/manufacturer basis.
@ -114,8 +113,6 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) :
}
}
L.e("Could not find volume for $data [tried: ${volumes.map { it.components }}]")
return null
}
@ -183,8 +180,6 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) :
val displayName = cursor.getString(displayNameIndex)
val volume = volumes.find { it.mediaStoreName == volumeName }
if (volume == null) {
L.e(
"Could not find volume for $volumeName:$relativePath/$displayName [tried: ${volumes.map { it.mediaStoreName }}]")
return null
}
val components = Components.parseUnix(relativePath).child(displayName)

View file

@ -24,7 +24,6 @@ import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull
import timber.log.Timber as L
data class MusicGraph(
val songVertex: List<SongVertex>,
@ -52,7 +51,6 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
override fun add(preSong: PreSong) {
val uid = preSong.computeUid()
if (songVertices.containsKey(uid)) {
L.d("Song ${preSong.path} already in graph at ${songVertices[uid]?.preSong?.path}")
return
}

View file

@ -26,7 +26,6 @@ import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.query.contentResolverSafe
import org.oxycblt.musikr.playlist.m3u.M3U
import timber.log.Timber as L
/**
* Generic playlist file importing abstraction.
@ -111,7 +110,6 @@ class ExternalPlaylistManagerImpl(
return ImportedPlaylist(newName, imported.paths)
}
} catch (e: Exception) {
L.e("Failed to import playlist: $e")
null
}
}
@ -125,17 +123,12 @@ class ExternalPlaylistManagerImpl(
filePath.directory
}
return try {
val outputStream = context.contentResolverSafe.openOutputStream(uri)
if (outputStream == null) {
L.e("Failed to export playlist: Could not open output stream")
return false
}
val outputStream = context.contentResolverSafe.openOutputStream(uri) ?: return false
outputStream.use {
m3u.write(playlist, it, workingDirectory, config)
true
}
} catch (e: Exception) {
L.e("Failed to export playlist: $e")
false
}
}

View file

@ -19,7 +19,6 @@
package org.oxycblt.musikr.playlist.m3u
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStream
@ -36,7 +35,6 @@ import org.oxycblt.musikr.playlist.PossiblePaths
import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.util.correctWhitespace
import org.oxycblt.musikr.util.unlikelyToBeNull
import timber.log.Timber as L
/**
* Minimal M3U file format implementation.
@ -75,12 +73,11 @@ interface M3U {
/** The mime type used for M3U files by the android system. */
const val MIME_TYPE = "audio/x-mpegurl"
fun from(context: Context): M3U = M3UImpl(context, VolumeManager.from(context))
fun from(context: Context): M3U = M3UImpl(VolumeManager.from(context))
}
}
private class M3UImpl(
@ApplicationContext private val context: Context,
private val volumeManager: VolumeManager
) : M3U {
override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? {
@ -117,15 +114,10 @@ private class M3UImpl(
}
}
if (path == null) {
L.e("Expected a path, instead got an EOF")
break@consumeFile
}
// There is basically no formal specification of file paths in M3U, and it differs
// based on the programs that generated it. I more or less have to consider any possible
// interpretation as valid.
val interpretations = interpretPath(path)
val interpretations = interpretPath(unlikelyToBeNull(path))
val possibilities =
interpretations.flatMap { expandInterpretation(it, workingDirectory, volumes) }

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.util
import java.security.MessageDigest
import java.util.UUID
import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.musikr.BuildConfig
import org.oxycblt.musikr.tag.Date
/**

View file

@ -20,4 +20,4 @@ apply from: file("media/core_settings.gradle")
rootProject.name = "Auxio"
include ':app'
include ':ktaglib'
include ':musikr'