diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f673813..f4536b681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ #### What's New - Added option to ignore `MediaStore` tags, allowing more correct metadata at the cost of longer loading times - - Added support for sort tags [#174, dependent on this feature] + - Added support for sort tags [#172, dependent on this feature] - Added Last Added sorting ## 2.5.0 diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index a056e751b..526d19e2b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -132,7 +132,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI // ViewPager2 will nominally consume window insets, which will then break the window // insets applied to the indexing view before API 30. Fix this by overriding the - // callback with a no-op listener. + // callback with a non-consuming listener. setOnApplyWindowInsetsListener { _, insets -> insets } } @@ -343,8 +343,6 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } private fun handleNavigation(item: Music?) { - // Note: You will want to add a post call to this if you want to re-introduce a collapsing - // toolbar. when (item) { is Song -> findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.album.id)) 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 227abaa45..79a058575 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -37,14 +37,11 @@ sealed class Music : Item() { abstract val rawSortName: String? /** - * The name of this item used for sorting. This will first use the sort tag for the item, - * followed by the name without a preceding article (The/A/An). In the case that the item has no - * name, this returns null. - * - * This should not be used outside of sorting and fast-scrolling. + * The name of this item used for sorting.This should not be used outside of sorting and + * fast-scrolling. */ val sortName: String? - get() = rawSortName ?: rawName?.withoutArticle + get() = rawSortName ?: rawName?.parseSortName() /** * Resolve a name from it's raw form to a form suitable to be shown in a ui. Ex. "unknown" would @@ -275,8 +272,9 @@ data class Genre(override val rawName: String?, override val songs: List) } } + // Sort tags don't make sense on genres override val rawSortName: String? - get() = null + get() = rawName override val id: Long get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong() 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 5bb0a3eaa..f87b6f04f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -55,42 +55,30 @@ val Long.audioUri: Uri val Long.albumCoverUri: Uri get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this) -/** - * Parse out the number field from a field assumed to be NN, where NN is a track number. This is - * most commonly found on vorbis comments. Values of zero will be ignored under the assumption that - * they are invalid. - */ -val String.plainTrackNo: Int? - get() = toIntOrNull()?.nonZeroOrNull() - /** * 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 * invalid. */ -val Int.packedTrackNo: Int? - get() = mod(1000).nonZeroOrNull() +fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull() /** * Parse out the disc 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 invalid. */ -val Int.packedDiscNo: Int? - get() = div(1000).nonZeroOrNull() +fun Int.unpackDiscNo() = div(1000).nonZeroOrNull() + +/** + * Parse out a plain number from a string. Values of 0 will be ignored under the assumption that + * they are invalid. + */ +fun String.parseNum() = toIntOrNull()?.nonZeroOrNull() /** * Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and * CD_TRACK_NUMBER. Values of zero will be ignored under the assumption that they are invalid. */ -val String.trackDiscNo: Int? - get() = split('/', limit = 2)[0].toIntOrNull()?.nonZeroOrNull() - -/** - * Parse out a plain year from a string. Values of 0 will be ignored under the assumption that they - * are invalid. - */ -val String.year: Int? - get() = toIntOrNull()?.nonZeroOrNull() +fun String.parsePositionNum() = split('/', limit = 2)[0].toIntOrNull()?.nonZeroOrNull() /** * Parse out the year field from a (presumably) ISO-8601-like date. This differs across tag formats @@ -98,8 +86,7 @@ val String.year: Int? * (...) and thus we can parse the year out by splitting at the first -. Values of 0 will be ignored * under the assumption that they are invalid. */ -val String.iso8601year: Int? - get() = split('-', limit = 2)[0].toIntOrNull()?.nonZeroOrNull() +fun String.parseIso8601Year() = split('-', limit = 2)[0].toIntOrNull()?.nonZeroOrNull() private fun Int.nonZeroOrNull() = if (this > 0) this else null @@ -108,29 +95,19 @@ private fun Int.nonZeroOrNull() = if (this > 0) this else null * anglo-centric, but it's also a bit of an expected feature in music players, so we implement it * anyway. */ -val String.withoutArticle: String - get() { - if (length > 5 && startsWith("the ", ignoreCase = true)) { - return slice(4..lastIndex) - } - - if (length > 4 && startsWith("an ", ignoreCase = true)) { - return slice(3..lastIndex) - } - - if (length > 3 && startsWith("a ", ignoreCase = true)) { - return slice(2..lastIndex) - } - - return this +fun String.parseSortName() = + when { + length > 5 && startsWith("the ", ignoreCase = true) -> substring(4) + length > 4 && startsWith("an ", ignoreCase = true) -> substring(3) + length > 3 && startsWith("a ", ignoreCase = true) -> substring(2) + else -> this } /** * Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map * that Auxio uses. */ -val String.id3GenreName: String - get() = parseId3v1Genre() ?: parseId3v2Genre() ?: this +fun String.parseId3GenreName() = parseId3v1Genre() ?: parseId3v2Genre() ?: this private fun String.parseId3v1Genre(): String? = when { @@ -158,7 +135,7 @@ private fun String.parseId3v2Genre(): String? { // ID3v1 tags. val genreIds = groups[1] if (genreIds != null && genreIds.value.isNotEmpty()) { - val ids = genreIds.value.substring(1 until genreIds.value.lastIndex).split(")(") + val ids = genreIds.value.substring(1).split(")(") for (id in ids) { id.parseId3v1Genre()?.let(genres::add) } 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 bd15f5e40..f1873a6ea 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 @@ -26,11 +26,10 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.vorbis.VorbisComment import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.audioUri -import org.oxycblt.auxio.music.id3GenreName -import org.oxycblt.auxio.music.iso8601year -import org.oxycblt.auxio.music.plainTrackNo -import org.oxycblt.auxio.music.trackDiscNo -import org.oxycblt.auxio.music.year +import org.oxycblt.auxio.music.parseId3GenreName +import org.oxycblt.auxio.music.parseIso8601Year +import org.oxycblt.auxio.music.parseNum +import org.oxycblt.auxio.music.parsePositionNum import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -211,10 +210,10 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { tags["TSOT"]?.let { audio.sortTitle = it } // Track, as NN/TT - tags["TRCK"]?.trackDiscNo?.let { audio.track = it } + tags["TRCK"]?.parsePositionNum()?.let { audio.track = it } // Disc, as NN/TT - tags["TPOS"]?.trackDiscNo?.let { audio.disc = it } + tags["TPOS"]?.parsePositionNum()?.let { audio.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 @@ -225,9 +224,9 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { // 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"]?.iso8601year - ?: tags["TDRC"]?.iso8601year ?: tags["TDRL"]?.iso8601year ?: tags["TORY"]?.year - ?: tags["TYER"]?.year) + (tags["TDOR"]?.parseIso8601Year() + ?: tags["TDRC"]?.parseIso8601Year() ?: tags["TDRL"]?.parseIso8601Year() + ?: tags["TORY"]?.parseNum() ?: tags["TYER"]?.parseNum()) ?.let { audio.year = it } // (Sort) Album @@ -243,7 +242,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { tags["TSO2"]?.let { audio.sortAlbumArtist = it } // Genre, with the weird ID3 rules. - tags["TCON"]?.let { audio.genre = it.id3GenreName } + tags["TCON"]?.let { audio.genre = it.parseId3GenreName() } } private fun populateVorbis(tags: Map) { @@ -252,10 +251,10 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { tags["TITLESORT"]?.let { audio.sortTitle = it } // Track. Probably not NN/TT, as TOTALTRACKS handles totals. - tags["TRACKNUMBER"]?.plainTrackNo?.let { audio.track = it } + tags["TRACKNUMBER"]?.parseNum()?.let { audio.track = it } // Disc. Probably not NN/TT, as TOTALDISCS handles totals. - tags["DISCNUMBER"]?.plainTrackNo?.let { audio.disc = it } + tags["DISCNUMBER"]?.parseNum()?.let { audio.disc = it } // Vorbis dates are less complicated, but there are still several types // Our hierarchy for dates is as such: @@ -263,7 +262,8 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { // 2. Date, as it is the most common date type // 3. Year, as old vorbis tags tended to use this (I know this because it's the only // tag that android supports, so it must be 15 years old or more!) - (tags["ORIGINALDATE"]?.iso8601year ?: tags["DATE"]?.iso8601year ?: tags["YEAR"]?.year) + (tags["ORIGINALDATE"]?.parseIso8601Year() + ?: tags["DATE"]?.parseIso8601Year() ?: tags["YEAR"]?.parseNum()) ?.let { audio.year = it } // (Sort) Album @@ -274,7 +274,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { tags["ARTIST"]?.let { audio.artist = it } tags["ARTISTSORT"]?.let { audio.sortArtist = it } - // (Sort) Album artist. + // (Sort) Album artist tags["ALBUMARTIST"]?.let { audio.albumArtist = it } tags["ALBUMARTISTSORT"]?.let { audio.sortAlbumArtist = it } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt index 08b7ff16e..4efac93aa 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt @@ -34,13 +34,13 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.albumCoverUri import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.directoryCompat -import org.oxycblt.auxio.music.id3GenreName import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat -import org.oxycblt.auxio.music.packedDiscNo -import org.oxycblt.auxio.music.packedTrackNo +import org.oxycblt.auxio.music.parseId3GenreName +import org.oxycblt.auxio.music.parsePositionNum import org.oxycblt.auxio.music.queryCursor import org.oxycblt.auxio.music.storageVolumesCompat -import org.oxycblt.auxio.music.trackDiscNo +import org.oxycblt.auxio.music.unpackDiscNo +import org.oxycblt.auxio.music.unpackTrackNo import org.oxycblt.auxio.music.useQuery import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.contentResolverSafe @@ -203,7 +203,7 @@ abstract class MediaStoreBackend : Indexer.Backend { // format a genre was derived from, we have to treat them like they are ID3 // genres, even when they might not be. val id = genreCursor.getLong(idIndex) - val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).id3GenreName + val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).parseId3GenreName() context.contentResolverSafe.useQuery( MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), @@ -350,40 +350,36 @@ abstract class MediaStoreBackend : Indexer.Backend { ) { fun toSong() = Song( - // Assert that the fields that should always exist are present. I can't confirm - // that - // every device provides these fields, but it seems likely that they do. - rawName = requireNotNull(title) { "Malformed audio: No title" }, - rawSortName = sortTitle, - path = - Path( - name = - requireNotNull(displayName) { "Malformed audio: No display name" }, - parent = - requireNotNull(dir) { "Malformed audio: No parent directory" }), - uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri, - mimeType = - MimeType( - fromExtension = - requireNotNull(extensionMimeType) { - "Malformed audio: No mime type" - }, - fromFormat = formatMimeType), - size = requireNotNull(size) { "Malformed audio: No size" }, - dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" }, - durationMs = requireNotNull(duration) { "Malformed audio: No duration" }, - track = track, - disc = disc, - _year = year, - _albumName = requireNotNull(album) { "Malformed audio: No album name" }, - _albumSortName = sortAlbum, - _albumCoverUri = - requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri, - _artistName = artist, - _artistSortName = sortArtist, - _albumArtistName = albumArtist, - _albumArtistSortName = sortAlbumArtist, - _genreName = genre) + // Assert that the fields that should always exist are present. I can't confirm + // that + // every device provides these fields, but it seems likely that they do. + rawName = requireNotNull(title) { "Malformed audio: No title" }, + rawSortName = sortTitle, + path = + Path( + name = requireNotNull(displayName) { "Malformed audio: No display name" }, + parent = requireNotNull(dir) { "Malformed audio: No parent directory" }), + uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri, + mimeType = + MimeType( + fromExtension = + requireNotNull(extensionMimeType) { "Malformed audio: No mime type" }, + fromFormat = formatMimeType), + size = requireNotNull(size) { "Malformed audio: No size" }, + dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" }, + durationMs = requireNotNull(duration) { "Malformed audio: No duration" }, + track = track, + disc = disc, + _year = year, + _albumName = requireNotNull(album) { "Malformed audio: No album name" }, + _albumSortName = sortAlbum, + _albumCoverUri = + requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri, + _artistName = artist, + _artistSortName = sortArtist, + _albumArtistName = albumArtist, + _albumArtistSortName = sortAlbumArtist, + _genreName = genre) } companion object { @@ -469,8 +465,8 @@ class Api21MediaStoreBackend : MediaStoreBackend() { val rawTrack = cursor.getIntOrNull(trackIndex) if (rawTrack != null) { - rawTrack.packedTrackNo?.let { audio.track = it } - rawTrack.packedDiscNo?.let { audio.disc = it } + rawTrack.unpackTrackNo()?.let { audio.track = it } + rawTrack.unpackDiscNo()?.let { audio.disc = it } } return audio @@ -555,8 +551,8 @@ open class Api29MediaStoreBackend : BaseApi29MediaStoreBackend() { // Use the old field instead. val rawTrack = cursor.getIntOrNull(trackIndex) if (rawTrack != null) { - rawTrack.packedTrackNo?.let { audio.track = it } - rawTrack.packedDiscNo?.let { audio.disc = it } + rawTrack.unpackTrackNo()?.let { audio.track = it } + rawTrack.unpackDiscNo()?.let { audio.disc = it } } return audio @@ -594,8 +590,8 @@ class Api30MediaStoreBackend : BaseApi29MediaStoreBackend() { // N is the number and T is the total. Parse the number while leaving out the // total, as we have no use for it. - cursor.getStringOrNull(trackIndex)?.trackDiscNo?.let { audio.track = it } - cursor.getStringOrNull(discIndex)?.trackDiscNo?.let { audio.disc = it } + cursor.getStringOrNull(trackIndex)?.parsePositionNum()?.let { audio.track = it } + cursor.getStringOrNull(discIndex)?.parsePositionNum()?.let { audio.disc = it } return audio }