all: break off musikr
This commit is contained in:
parent
f33377cf26
commit
e908d0e102
84 changed files with 151 additions and 286 deletions
5
.gitmodules
vendored
5
.gitmodules
vendored
|
@ -1,7 +1,8 @@
|
||||||
[submodule "media"]
|
[submodule "media"]
|
||||||
path = media
|
path = media
|
||||||
url = https://github.com/OxygenCobalt/media.git
|
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
|
url = https://github.com/taglib/taglib.git
|
||||||
tag = v2.0.2
|
tag = v2.0.2
|
||||||
|
|
|
@ -2,7 +2,6 @@ plugins {
|
||||||
id "com.android.application"
|
id "com.android.application"
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "androidx.navigation.safeargs.kotlin"
|
id "androidx.navigation.safeargs.kotlin"
|
||||||
id "com.diffplug.spotless"
|
|
||||||
id "kotlin-parcelize"
|
id "kotlin-parcelize"
|
||||||
id "dagger.hilt.android.plugin"
|
id "dagger.hilt.android.plugin"
|
||||||
id "kotlin-kapt"
|
id "kotlin-kapt"
|
||||||
|
@ -12,11 +11,9 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 35
|
compileSdk 35
|
||||||
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify
|
// Auxio implicitly depends on the native modules, explicitly specify it
|
||||||
// it here so that binary stripping will work.
|
// here so the libraries are still stripped.
|
||||||
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
|
ndkVersion ndk_version
|
||||||
// NDK use is unified
|
|
||||||
ndkVersion "26.3.11579264"
|
|
||||||
namespace "org.oxycblt.auxio"
|
namespace "org.oxycblt.auxio"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
@ -24,8 +21,8 @@ android {
|
||||||
versionName "3.6.3"
|
versionName "3.6.3"
|
||||||
versionCode 53
|
versionCode 53
|
||||||
|
|
||||||
minSdk 24
|
minSdk target_sdk
|
||||||
targetSdk 35
|
targetSdk target_sdk
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
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-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
def coroutines_version = '1.7.2'
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlin_coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version"
|
|
||||||
|
|
||||||
// --- SUPPORT ---
|
// --- SUPPORT ---
|
||||||
|
|
||||||
|
@ -125,20 +121,23 @@ dependencies {
|
||||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
def room_version = '2.6.1'
|
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
ksp "androidx.room:room-compiler:$room_version"
|
ksp "androidx.room:room-compiler:$room_version"
|
||||||
implementation "androidx.room:room-ktx:$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 ---
|
// --- THIRD PARTY ---
|
||||||
|
|
||||||
// Exoplayer (Vendored)
|
// Exoplayer (Vendored)
|
||||||
implementation project(":media-lib-exoplayer")
|
implementation project(":media-lib-exoplayer")
|
||||||
implementation project(":media-lib-decoder-ffmpeg")
|
implementation project(":media-lib-decoder-ffmpeg")
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.3"
|
|
||||||
|
|
||||||
// Taglib
|
|
||||||
implementation project(":ktaglib")
|
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation 'io.coil-kt.coil3:coil-core:3.0.2'
|
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.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
|
||||||
kotlin {
|
|
||||||
target "src/**/*.kt"
|
|
||||||
ktfmt().dropboxStyle()
|
|
||||||
licenseHeaderFile("NOTICE")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
preDebugBuild.dependsOn spotlessApply
|
|
||||||
}
|
|
||||||
|
|
|
@ -54,7 +54,6 @@ import org.oxycblt.musikr.Music
|
||||||
import org.oxycblt.musikr.MusicParent
|
import org.oxycblt.musikr.MusicParent
|
||||||
import org.oxycblt.musikr.Playlist
|
import org.oxycblt.musikr.Playlist
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
import org.oxycblt.musikr.metadata.AudioProperties
|
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,7 +68,6 @@ class DetailViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val listSettings: ListSettings,
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val audioPropertiesFactory: AudioProperties.Factory,
|
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
detailGeneratorFactory: DetailGenerator.Factory
|
detailGeneratorFactory: DetailGenerator.Factory
|
||||||
) : ViewModel(), DetailGenerator.Invalidator {
|
) : ViewModel(), DetailGenerator.Invalidator {
|
||||||
|
@ -89,10 +87,6 @@ constructor(
|
||||||
val currentSong: StateFlow<Song?>
|
val currentSong: StateFlow<Song?>
|
||||||
get() = _currentSong
|
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 ---
|
// --- ALBUM ---
|
||||||
|
|
||||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
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].
|
* be updated to align with the new [Song].
|
||||||
*
|
*
|
||||||
* @param uid The UID of the [Song] to load. Must be valid.
|
* @param uid The UID of the [Song] to load. Must be valid.
|
||||||
|
@ -511,17 +505,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAudioInfo(song: Song) {
|
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(
|
private inline fun <T : MusicParent> refreshDetail(
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.concatLocalized
|
import org.oxycblt.auxio.util.concatLocalized
|
||||||
import org.oxycblt.musikr.Music
|
import org.oxycblt.musikr.Music
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
import org.oxycblt.musikr.metadata.AudioProperties
|
|
||||||
import org.oxycblt.musikr.tag.Name
|
import org.oxycblt.musikr.tag.Name
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
|
@ -72,63 +71,63 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setSong(args.songUid)
|
detailModel.setSong(args.songUid)
|
||||||
detailModel.toShow.consume()
|
detailModel.toShow.consume()
|
||||||
collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong)
|
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?, info: AudioProperties?) {
|
private fun updateSong(song: Song?) {
|
||||||
if (song == null) {
|
// if (song == null) {
|
||||||
L.d("No song to show, navigating away")
|
L.d("No song to show, navigating away")
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (info != null) {
|
// if (info != null) {
|
||||||
val context = requireContext()
|
// val context = requireContext()
|
||||||
detailAdapter.update(
|
// detailAdapter.update(
|
||||||
buildList {
|
// buildList {
|
||||||
add(SongProperty(R.string.lbl_name, song.zipName(context)))
|
// add(SongProperty(R.string.lbl_name, song.zipName(context)))
|
||||||
add(SongProperty(R.string.lbl_album, song.album.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_artists, song.artists.zipNames(context)))
|
||||||
add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context)))
|
// add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context)))
|
||||||
song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) }
|
// song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) }
|
||||||
song.track?.let {
|
// song.track?.let {
|
||||||
add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
|
// add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
|
||||||
}
|
// }
|
||||||
song.disc?.let {
|
// song.disc?.let {
|
||||||
val formattedNumber = getString(R.string.fmt_number, it.number)
|
// val formattedNumber = getString(R.string.fmt_number, it.number)
|
||||||
val zipped =
|
// val zipped =
|
||||||
if (it.name != null) {
|
// if (it.name != null) {
|
||||||
getString(R.string.fmt_zipped_names, formattedNumber, it.name)
|
// getString(R.string.fmt_zipped_names, formattedNumber, it.name)
|
||||||
} else {
|
// } else {
|
||||||
formattedNumber
|
// formattedNumber
|
||||||
}
|
// }
|
||||||
add(SongProperty(R.string.lbl_disc, zipped))
|
// add(SongProperty(R.string.lbl_disc, zipped))
|
||||||
}
|
// }
|
||||||
add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
|
// add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
|
||||||
// info.format.resolveName(context)?.let {
|
// // info.format.resolveName(context)?.let {
|
||||||
// add(SongProperty(R.string.lbl_format, it))
|
// // add(SongProperty(R.string.lbl_format, it))
|
||||||
// }
|
// // }
|
||||||
add(
|
// add(
|
||||||
SongProperty(
|
// SongProperty(
|
||||||
R.string.lbl_size, Formatter.formatFileSize(context, song.size)))
|
// R.string.lbl_size, Formatter.formatFileSize(context, song.size)))
|
||||||
add(SongProperty(R.string.lbl_duration, song.durationMs.formatDurationMs(true)))
|
// add(SongProperty(R.string.lbl_duration, song.durationMs.formatDurationMs(true)))
|
||||||
info.bitrateKbps?.let {
|
// info.bitrateKbps?.let {
|
||||||
add(SongProperty(R.string.lbl_bitrate, getString(R.string.fmt_bitrate, it)))
|
// add(SongProperty(R.string.lbl_bitrate, getString(R.string.fmt_bitrate, it)))
|
||||||
}
|
// }
|
||||||
info.sampleRateHz?.let {
|
// info.sampleRateHz?.let {
|
||||||
add(
|
// add(
|
||||||
SongProperty(
|
// SongProperty(
|
||||||
R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
|
// R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
|
||||||
}
|
// }
|
||||||
song.replayGainAdjustment.track?.let {
|
// song.replayGainAdjustment.track?.let {
|
||||||
add(SongProperty(R.string.lbl_replaygain_track, it.formatDb(context)))
|
// add(SongProperty(R.string.lbl_replaygain_track, it.formatDb(context)))
|
||||||
}
|
// }
|
||||||
song.replayGainAdjustment.album?.let {
|
// song.replayGainAdjustment.album?.let {
|
||||||
add(SongProperty(R.string.lbl_replaygain_album, it.formatDb(context)))
|
// add(SongProperty(R.string.lbl_replaygain_album, it.formatDb(context)))
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
UpdateInstructions.Replace(0))
|
// UpdateInstructions.Replace(0))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Music> T.zipName(context: Context): String {
|
private fun <T : Music> T.zipName(context: Context): String {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
33
build.gradle
33
build.gradle
|
@ -1,8 +1,16 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
|
agp_version = '8.7.3'
|
||||||
kotlin_version = '2.0.21'
|
kotlin_version = '2.0.21'
|
||||||
|
kotlin_coroutines_version = '1.7.2'
|
||||||
navigation_version = "2.8.3"
|
navigation_version = "2.8.3"
|
||||||
hilt_version = '2.51.1'
|
hilt_version = '2.51.1'
|
||||||
|
room_version = '2.6.1'
|
||||||
|
|
||||||
|
min_sdk = 24
|
||||||
|
target_sdk = 35
|
||||||
|
ndk_version = "27.2.12479018"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -12,14 +20,27 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
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
|
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 "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.google.devtools.ksp" version '2.0.21-1.0.25' apply false
|
||||||
id "com.diffplug.spotless" version "6.25.0" apply false
|
id "com.diffplug.spotless" version "6.25.0" apply true
|
||||||
id 'com.android.library' version '8.7.2' apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('clean', Delete) {
|
spotless {
|
||||||
delete rootProject.buildDir
|
kotlin {
|
||||||
}
|
target "*/src/**/*.kt"
|
||||||
|
ktfmt().dropboxStyle()
|
||||||
|
licenseHeaderFile("NOTICE")
|
||||||
|
}
|
||||||
|
|
||||||
|
cpp {
|
||||||
|
target "*/src/**/*.cpp"
|
||||||
|
clangFormat()
|
||||||
|
licenseHeaderFile("NOTICE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
0
ktaglib/.gitignore → musikr/.gitignore
vendored
0
ktaglib/.gitignore → musikr/.gitignore
vendored
|
@ -3,15 +3,19 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.library'
|
id 'com.android.library'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id "com.google.devtools.ksp"
|
||||||
|
id "com.diffplug.spotless"
|
||||||
|
id "kotlin-parcelize"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'org.oxycblt.ktaglib'
|
namespace 'org.oxycblt.musikr'
|
||||||
compileSdk 34
|
compileSdk target_sdk
|
||||||
ndkVersion "26.3.11579264"
|
ndkVersion "$ndk_version"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 24
|
minSdk min_sdk
|
||||||
|
targetSdk target_sdk
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles "consumer-rules.pro"
|
consumerProguardFiles "consumer-rules.pro"
|
||||||
|
@ -28,19 +32,45 @@ android {
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path "src/main/cpp/CMakeLists.txt"
|
path "src/main/cpp/CMakeLists.txt"
|
||||||
version "3.22.1"
|
version "3.22.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
coreLibraryDesugaringEnabled true
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
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) {
|
task assembleTaglib(type: Exec) {
|
0
musikr/consumer-rules.pro
Normal file
0
musikr/consumer-rules.pro
Normal file
|
@ -80,7 +80,7 @@ public:
|
||||||
/*!
|
/*!
|
||||||
* Reset the end-of-stream and error flags on the stream.
|
* Reset the end-of-stream and error flags on the stream.
|
||||||
*/
|
*/
|
||||||
void clear();
|
void clear() override;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Returns the current offset within the stream.
|
* Returns the current offset within the stream.
|
|
@ -27,14 +27,16 @@ build_for_arch() {
|
||||||
cd $TAGLIB_SRC_DIR
|
cd $TAGLIB_SRC_DIR
|
||||||
cmake -B $DST_DIR -DANDROID_NDK_PATH=${NDK_PATH} -DCMAKE_TOOLCHAIN_FILE=${NDK_TOOLCHAIN} \
|
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 \
|
-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
|
-DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DWITH_ZLIB=OFF -DCMAKE_BUILD_TYPE=Release \
|
||||||
cmake --build $DST_DIR --config Release
|
-DCMAKE_CXX_FLAGS="-fPIC"
|
||||||
|
cmake --build $DST_DIR --config Release -j$(nproc)
|
||||||
cd $WORKING_DIR
|
cd $WORKING_DIR
|
||||||
|
|
||||||
cmake --install $DST_DIR --config Release --prefix $PKG_DIR --strip
|
cmake --install $DST_DIR --config Release --prefix $PKG_DIR --strip
|
||||||
}
|
}
|
||||||
|
|
||||||
build_for_arch $X86_ARCH
|
build_for_arch $X86_ARCH&
|
||||||
build_for_arch $X86_64_ARCH
|
build_for_arch $X86_64_ARCH&
|
||||||
build_for_arch $ARMV7_ARCH
|
build_for_arch $ARMV7_ARCH&
|
||||||
build_for_arch $ARMV8_ARCH
|
build_for_arch $ARMV8_ARCH&
|
||||||
|
wait
|
|
@ -22,7 +22,6 @@ import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
|
@ -78,7 +77,7 @@ interface DocumentPathFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DocumentPathFactoryImpl(
|
private class DocumentPathFactoryImpl(
|
||||||
@ApplicationContext private val context: Context,
|
private val context: Context,
|
||||||
private val volumeManager: VolumeManager,
|
private val volumeManager: VolumeManager,
|
||||||
private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory
|
private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory
|
||||||
) : DocumentPathFactory {
|
) : DocumentPathFactory {
|
|
@ -23,7 +23,6 @@ import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
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.
|
* 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,8 +180,6 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) :
|
||||||
val displayName = cursor.getString(displayNameIndex)
|
val displayName = cursor.getString(displayNameIndex)
|
||||||
val volume = volumes.find { it.mediaStoreName == volumeName }
|
val volume = volumes.find { it.mediaStoreName == volumeName }
|
||||||
if (volume == null) {
|
if (volume == null) {
|
||||||
L.e(
|
|
||||||
"Could not find volume for $volumeName:$relativePath/$displayName [tried: ${volumes.map { it.mediaStoreName }}]")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val components = Components.parseUnix(relativePath).child(displayName)
|
val components = Components.parseUnix(relativePath).child(displayName)
|
|
@ -24,7 +24,6 @@ import org.oxycblt.musikr.tag.interpret.PreArtist
|
||||||
import org.oxycblt.musikr.tag.interpret.PreGenre
|
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
|
||||||
import timber.log.Timber as L
|
|
||||||
|
|
||||||
data class MusicGraph(
|
data class MusicGraph(
|
||||||
val songVertex: List<SongVertex>,
|
val songVertex: List<SongVertex>,
|
||||||
|
@ -52,7 +51,6 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
|
||||||
override fun add(preSong: PreSong) {
|
override fun add(preSong: PreSong) {
|
||||||
val uid = preSong.computeUid()
|
val uid = preSong.computeUid()
|
||||||
if (songVertices.containsKey(uid)) {
|
if (songVertices.containsKey(uid)) {
|
||||||
L.d("Song ${preSong.path} already in graph at ${songVertices[uid]?.preSong?.path}")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
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.playlist.m3u.M3U
|
import org.oxycblt.musikr.playlist.m3u.M3U
|
||||||
import timber.log.Timber as L
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic playlist file importing abstraction.
|
* Generic playlist file importing abstraction.
|
||||||
|
@ -111,7 +110,6 @@ class ExternalPlaylistManagerImpl(
|
||||||
return ImportedPlaylist(newName, imported.paths)
|
return ImportedPlaylist(newName, imported.paths)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e("Failed to import playlist: $e")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,17 +123,12 @@ class ExternalPlaylistManagerImpl(
|
||||||
filePath.directory
|
filePath.directory
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
val outputStream = context.contentResolverSafe.openOutputStream(uri)
|
val outputStream = context.contentResolverSafe.openOutputStream(uri) ?: return false
|
||||||
if (outputStream == null) {
|
|
||||||
L.e("Failed to export playlist: Could not open output stream")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
outputStream.use {
|
outputStream.use {
|
||||||
m3u.write(playlist, it, workingDirectory, config)
|
m3u.write(playlist, it, workingDirectory, config)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
L.e("Failed to export playlist: $e")
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,7 +19,6 @@
|
||||||
package org.oxycblt.musikr.playlist.m3u
|
package org.oxycblt.musikr.playlist.m3u
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.BufferedWriter
|
import java.io.BufferedWriter
|
||||||
import java.io.InputStream
|
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.Name
|
||||||
import org.oxycblt.musikr.tag.util.correctWhitespace
|
import org.oxycblt.musikr.tag.util.correctWhitespace
|
||||||
import org.oxycblt.musikr.util.unlikelyToBeNull
|
import org.oxycblt.musikr.util.unlikelyToBeNull
|
||||||
import timber.log.Timber as L
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal M3U file format implementation.
|
* Minimal M3U file format implementation.
|
||||||
|
@ -75,12 +73,11 @@ interface M3U {
|
||||||
/** The mime type used for M3U files by the android system. */
|
/** The mime type used for M3U files by the android system. */
|
||||||
const val MIME_TYPE = "audio/x-mpegurl"
|
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(
|
private class M3UImpl(
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val volumeManager: VolumeManager
|
private val volumeManager: VolumeManager
|
||||||
) : M3U {
|
) : M3U {
|
||||||
override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? {
|
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
|
// 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
|
// based on the programs that generated it. I more or less have to consider any possible
|
||||||
// interpretation as valid.
|
// interpretation as valid.
|
||||||
val interpretations = interpretPath(path)
|
val interpretations = interpretPath(unlikelyToBeNull(path))
|
||||||
val possibilities =
|
val possibilities =
|
||||||
interpretations.flatMap { expandInterpretation(it, workingDirectory, volumes) }
|
interpretations.flatMap { expandInterpretation(it, workingDirectory, volumes) }
|
||||||
|
|
|
@ -21,7 +21,7 @@ package org.oxycblt.musikr.util
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.musikr.BuildConfig
|
||||||
import org.oxycblt.musikr.tag.Date
|
import org.oxycblt.musikr.tag.Date
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -20,4 +20,4 @@ apply from: file("media/core_settings.gradle")
|
||||||
|
|
||||||
rootProject.name = "Auxio"
|
rootProject.name = "Auxio"
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':ktaglib'
|
include ':musikr'
|
||||||
|
|
Loading…
Reference in a new issue