diff --git a/app/build.gradle b/app/build.gradle index f1907693a..7ea0c6819 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,9 +56,6 @@ android { } } -afterEvaluate { - preDebugBuild.dependsOn spotlessApply -} dependencies { // Kotlin @@ -103,8 +100,11 @@ dependencies { // Exoplayer // WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE PRE-BUILD SCRIPT. // IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. - implementation "com.google.android.exoplayer:exoplayer-core:2.18.1" + implementation("com.google.android.exoplayer:exoplayer-core:2.18.1") { + exclude group: "com.google.android.exoplayer", module: "exoplayer-extractor" + } + implementation fileTree(dir: "libs", include: ["library-*.aar"]) implementation fileTree(dir: "libs", include: ["extension-*.aar"]) // Image loading diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index aadef17b4..810a89d63 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -34,8 +34,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.util.inRangeOrNull -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.msToSecs import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -86,75 +84,31 @@ sealed class Music : Item { * @author OxygenCobalt */ @Parcelize - class UID - private constructor(private val datatype: String, private val isMusicBrainz: Boolean, private val uuid: UUID) : - Parcelable { - // TODO: Formalize datatype and isMusicBrainz more - + class UID private constructor(private val tag: String, private val uuid: UUID) : Parcelable { // Cache the hashCode for speed - @IgnoredOnParcel private val hashCode: Int - - init { - var result = datatype.hashCode() - result = 31 * result + isMusicBrainz.hashCode() - result = 31 * result + uuid.hashCode() - hashCode = result - } + @IgnoredOnParcel private val hashCode = 31 * tag.hashCode() + uuid.hashCode() override fun hashCode() = hashCode - override fun equals(other: Any?) = - other is UID && - datatype == other.datatype && - isMusicBrainz == other.isMusicBrainz && - uuid == other.uuid + override fun equals(other: Any?) = other is UID && tag == other.tag && uuid == other.uuid - override fun toString() = - "$datatype/${if (isMusicBrainz) FORMAT_MUSICBRAINZ else FORMAT_AUXIO}:$uuid" + override fun toString() = "$tag:$uuid" companion object { - const val FORMAT_AUXIO = "auxio" - const val FORMAT_MUSICBRAINZ = "musicbrainz" - /** Parse a [UID] from the string [uid]. Returns null if not valid. */ fun fromString(uid: String): UID? { val split = uid.split(':', limit = 2) if (split.size != 2) { - logE("Invalid uid: Malformed structure") - } - - val namespace = split[0].split('/', limit = 2) - if (namespace.size != 2) { - logE("Invalid uid: Malformed namespace") return null } - val datatype = namespace[0] - val isMusicBrainz = - when (namespace[1]) { - FORMAT_AUXIO -> false - FORMAT_MUSICBRAINZ -> true - else -> { - logE("Invalid uid: Malformed uuid format") - return null - } - } - - val uuid = - try { - UUID.fromString(split[1]) - } catch (e: IllegalArgumentException) { - logE("Invalid uid: Malformed uuid") - return null - } - - return UID(datatype, isMusicBrainz, uuid) + return UID(tag = split[0], split[1].toUuid() ?: return null) } /** * Make a UUID derived from the MD5 hash of the data digested in [updates]. * - * This is considered the "auxio" uuid format. + * This is Auxio's UID format. */ fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID { // Auxio hashes consist of the MD5 hash of the non-subjective, consistent @@ -162,7 +116,8 @@ sealed class Music : Item { val digest = MessageDigest.getInstance("MD5") updates(digest) val uuid = digest.digest().toUuid() - return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid) + val tag = "auxio.${unlikelyToBeNull(clazz.simpleName).lowercase()}" + return UID(tag, uuid) } } } @@ -298,9 +253,6 @@ class Song constructor(private val raw: Raw) : Music() { update(track) update(disc) - - // Hashing by seconds makes the song more resilient to trimming - update(durationMs.msToSecs()) } } @@ -580,9 +532,9 @@ fun ByteArray.toUuid(): UUID { * nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle * or reject valid-ish dates. * - * Date instances are immutable and their implementation is hidden. To instantiate one, use - * [from]. The string representation of a Date is RFC 3339, with granular position depending on the - * presence of particular tokens. + * Date instances are immutable and their implementation is hidden. To instantiate one, use [from]. + * The string representation of a Date is RFC 3339, with granular position depending on the presence + * of particular tokens. * * Please, **Do not use this for anything important related to time.** I cannot stress this enough. * This code will blow up if you try to do that. @@ -776,8 +728,6 @@ sealed class ReleaseType { } companion object { - fun parse(type: String) = parse(type.split('+')) - fun parse(types: List): ReleaseType { val primary = types[0].trim() diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index 7346d26c4..d51c9f32f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -26,6 +26,7 @@ import android.provider.MediaStore import androidx.core.text.isDigitsOnly import org.oxycblt.auxio.R import org.oxycblt.auxio.util.nonZeroOrNull +import java.util.UUID /** Shortcut for making a [ContentResolver] query with less superfluous arguments. */ fun ContentResolver.queryCursor( @@ -58,6 +59,8 @@ val Long.audioUri: Uri val Long.albumCoverUri: Uri get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this) +fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null } + /** * Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc * and T is the track number. Values of zero will be ignored under the assumption that they are @@ -100,9 +103,6 @@ fun String.parseSortName() = else -> this } -/** Shortcut to parse an [ReleaseType] from a string */ -fun String.parseReleaseType() = ReleaseType.parse(this) - /** Shortcut to parse a [ReleaseType] from a list of strings */ fun List.parseReleaseType() = ReleaseType.parse(this) @@ -110,8 +110,14 @@ fun List.parseReleaseType() = ReleaseType.parse(this) * Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map * that Auxio uses. */ -fun String.parseId3GenreName() = - parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: listOf(this) +fun String.parseId3GenreName() = parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: listOf(this) + +/** + * Decodes the genre names from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map + * that Auxio uses. + */ +fun List.parseId3GenreName() = flatMap { it.parseId3GenreName() } + private fun String.parseId3v1Genre(): String? = when { diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt index 18dd0b9fd..a20d44233 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt @@ -169,7 +169,7 @@ class Task(context: Context, private val raw: Song.Raw) { } private fun completeAudio(metadata: Metadata) { - val id3v2Tags = mutableMapOf() + val id3v2Tags = mutableMapOf>() val vorbisTags = mutableMapOf>() // ExoPlayer only exposes ID3v2 and Vorbis metadata, which constitutes the vast majority @@ -179,9 +179,9 @@ class Task(context: Context, private val raw: Song.Raw) { when (val tag = metadata[i]) { is TextInformationFrame -> { val id = tag.description?.let { "TXXX:${it.sanitize()}" } ?: tag.id.sanitize() - val value = tag.value.sanitize() - if (value.isNotEmpty()) { - id3v2Tags[id] = value + val values = tag.values.map { it.sanitize() } + if (values.isNotEmpty() && values.all { it.isNotEmpty() }) { + id3v2Tags[id] = values } } is VorbisComment -> { @@ -207,16 +207,16 @@ class Task(context: Context, private val raw: Song.Raw) { } } - private fun populateId3v2(tags: Map) { + private fun populateId3v2(tags: Map>) { // (Sort) Title - tags["TIT2"]?.let { raw.name = it } - tags["TSOT"]?.let { raw.sortName = it } + tags["TIT2"]?.let { raw.name = it[0] } + tags["TSOT"]?.let { raw.sortName = it[0] } // Track, as NN/TT - tags["TRCK"]?.parsePositionNum()?.let { raw.track = it } + tags["TRCK"]?.run { get(0).parsePositionNum() }?.let { raw.track = it } // Disc, as NN/TT - tags["TPOS"]?.parsePositionNum()?.let { raw.disc = it } + tags["TPOS"]?.run { get(0).parsePositionNum() } ?.let { raw.disc = it } // 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 @@ -227,22 +227,23 @@ class Task(context: Context, private val raw: Song.Raw) { // 3. ID3v2.4 Release Date, as it is the second most common date type // 4. ID3v2.3 Original Date, as it is like #1 // 5. ID3v2.3 Release Year, as it is the most common date type - (tags["TDOR"]?.parseTimestamp() - ?: tags["TDRC"]?.parseTimestamp() ?: tags["TDRL"]?.parseTimestamp() + (tags["TDOR"]?.run { get(0).parseTimestamp() } + ?: tags["TDRC"]?.run { get(0).parseTimestamp() } + ?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags)) ?.let { raw.date = it } // (Sort) Album - tags["TALB"]?.let { raw.albumName = it } - tags["TSOA"]?.let { raw.albumSortName = it } + tags["TALB"]?.let { raw.albumName = it[0] } + tags["TSOA"]?.let { raw.albumSortName = it[0] } // (Sort) Artist - tags["TPE1"]?.let { raw.artistName = it } - tags["TSOP"]?.let { raw.artistSortName = it } + tags["TPE1"]?.let { raw.artistName = it.joinToString() } + tags["TSOP"]?.let { raw.artistSortName = it.joinToString() } // (Sort) Album artist - tags["TPE2"]?.let { raw.albumArtistName = it } - tags["TSO2"]?.let { raw.albumArtistSortName = it } + tags["TPE2"]?.let { raw.albumArtistName = it.joinToString() } + tags["TSO2"]?.let { raw.albumArtistSortName = it.joinToString() } // Genre, with the weird ID3 rules. tags["TCON"]?.let { raw.genreNames = it.parseId3GenreName() } @@ -253,18 +254,18 @@ class Task(context: Context, private val raw: Song.Raw) { } } - private fun parseId3v23Date(tags: Map): Date? { - val year = tags["TORY"]?.toIntOrNull() ?: tags["TYER"]?.toIntOrNull() ?: return null + private fun parseId3v23Date(tags: Map>): Date? { + val year = tags["TORY"]?.run { get(0).toIntOrNull() } ?: tags["TYER"]?.run { get(0).toIntOrNull() } ?: return null - val mmdd = tags["TDAT"] - return if (mmdd != null && mmdd.length == 4 && mmdd.isDigitsOnly()) { - val mm = mmdd.substring(0..1).toInt() - val dd = mmdd.substring(2..3).toInt() + val tdat = tags["TDAT"] + return if (tdat != null && tdat[0].length == 4 && tdat[0].isDigitsOnly()) { + val mm = tdat[0].substring(0..1).toInt() + val dd = tdat[0].substring(2..3).toInt() - val hhmi = tags["TIME"] - if (hhmi != null && hhmi.length == 4 && hhmi.isDigitsOnly()) { - val hh = hhmi.substring(0..1).toInt() - val mi = hhmi.substring(2..3).toInt() + val time = tags["TIME"] + if (time != null && time[0].length == 4 && time[0].isDigitsOnly()) { + val hh = time[0].substring(0..1).toInt() + val mi = time[0].substring(2..3).toInt() Date.from(year, mm, dd, hh, mi) } else { Date.from(year, mm, dd) @@ -297,8 +298,8 @@ class Task(context: Context, private val raw: Song.Raw) { ?.let { raw.date = it } // (Sort) Album - tags["ALBUM"]?.let { raw.albumName = it.joinToString() } - tags["ALBUMSORT"]?.let { raw.albumSortName = it.joinToString() } + tags["ALBUM"]?.let { raw.albumName = it[0] } + tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] } // (Sort) Artist tags["ARTIST"]?.let { raw.artistName = it.joinToString() } diff --git a/prebuild.py b/prebuild.py index 659718017..efd8e4e79 100755 --- a/prebuild.py +++ b/prebuild.py @@ -19,7 +19,7 @@ import re # WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND # THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. -EXO_VERSION = "2.18.1" +# EXO_VERSION = "2.18.1" FLAC_VERSION = "1.3.2" FATAL="\033[1;31m" @@ -95,9 +95,9 @@ sh("rm -rf " + exoplayer_path) sh("rm -rf " + libs_path) print(INFO + "info:" + NC + " cloning exoplayer...") -sh("git clone https://github.com/google/ExoPlayer.git " + exoplayer_path) +sh("git clone https://github.com/OxygenCobalt/ExoPlayer.git " + exoplayer_path) os.chdir(exoplayer_path) -sh("git checkout r" + EXO_VERSION) +sh("git checkout auxio") print(INFO + "info:" + NC + " assembling flac extension...") flac_ext_aar_path = os.path.join(exoplayer_path, "extensions", "flac", @@ -111,9 +111,17 @@ sh(ndk_build_path + " APP_ABI=all -j4") os.chdir(exoplayer_path) sh("./gradlew extension-flac:bundleReleaseAar") + +print(INFO + "info:" + NC + " assembling extractor component...") + +extractor_aar_path = os.path.join(exoplayer_path, "library", "extractor", + "buildout", "outputs", "aar", "library-extractor-release.aar") + +sh("./gradlew library-extractor:bundleReleaseAar") os.chdir(start_path) sh("mkdir " + libs_path) sh("cp " + flac_ext_aar_path + " " + libs_path) - +sh("cp " + extractor_aar_path + " " + libs_path) + print(OK + "success:" + NC + " completed pre-build")