music: make field utils functions

Make some field utils functions, as they do work.
This commit is contained in:
OxygenCobalt 2022-07-14 11:43:28 -06:00
parent 969c0c69b7
commit 7833ec4460
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 81 additions and 112 deletions

View file

@ -5,7 +5,7 @@
#### What's New #### What's New
- Added option to ignore `MediaStore` tags, allowing more correct metadata - Added option to ignore `MediaStore` tags, allowing more correct metadata
at the cost of longer loading times 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 - Added Last Added sorting
## 2.5.0 ## 2.5.0

View file

@ -132,7 +132,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// ViewPager2 will nominally consume window insets, which will then break the window // 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 // 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 } setOnApplyWindowInsetsListener { _, insets -> insets }
} }
@ -343,8 +343,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
private fun handleNavigation(item: Music?) { 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) { when (item) {
is Song -> is Song ->
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.album.id)) findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.album.id))

View file

@ -37,14 +37,11 @@ sealed class Music : Item() {
abstract val rawSortName: String? abstract val rawSortName: String?
/** /**
* The name of this item used for sorting. This will first use the sort tag for the item, * The name of this item used for sorting.This should not be used outside of sorting and
* followed by the name without a preceding article (The/A/An). In the case that the item has no * fast-scrolling.
* name, this returns null.
*
* This should not be used outside of sorting and fast-scrolling.
*/ */
val sortName: String? 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 * 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<Song>)
} }
} }
// Sort tags don't make sense on genres
override val rawSortName: String? override val rawSortName: String?
get() = null get() = rawName
override val id: Long override val id: Long
get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong() get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong()

View file

@ -55,42 +55,30 @@ val Long.audioUri: Uri
val Long.albumCoverUri: Uri val Long.albumCoverUri: Uri
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this) 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 * 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 * and T is the track number. Values of zero will be ignored under the assumption that they are
* invalid. * invalid.
*/ */
val Int.packedTrackNo: Int? fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
get() = mod(1000).nonZeroOrNull()
/** /**
* Parse out the disc number field as if the given Int is formatted as DTTT, where D Is the disc and * 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. * T is the track number. Values of zero will be ignored under the assumption that they are invalid.
*/ */
val Int.packedDiscNo: Int? fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
get() = 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 * 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. * CD_TRACK_NUMBER. Values of zero will be ignored under the assumption that they are invalid.
*/ */
val String.trackDiscNo: Int? fun String.parsePositionNum() = split('/', limit = 2)[0].toIntOrNull()?.nonZeroOrNull()
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()
/** /**
* Parse out the year field from a (presumably) ISO-8601-like date. This differs across tag formats * 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 * (...) 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. * under the assumption that they are invalid.
*/ */
val String.iso8601year: Int? fun String.parseIso8601Year() = split('-', limit = 2)[0].toIntOrNull()?.nonZeroOrNull()
get() = split('-', limit = 2)[0].toIntOrNull()?.nonZeroOrNull()
private fun Int.nonZeroOrNull() = if (this > 0) this else null 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 * anglo-centric, but it's also a bit of an expected feature in music players, so we implement it
* anyway. * anyway.
*/ */
val String.withoutArticle: String fun String.parseSortName() =
get() { when {
if (length > 5 && startsWith("the ", ignoreCase = true)) { length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
return slice(4..lastIndex) length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
} length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
if (length > 4 && startsWith("an ", ignoreCase = true)) {
return slice(3..lastIndex)
}
if (length > 3 && startsWith("a ", ignoreCase = true)) {
return slice(2..lastIndex)
}
return this
} }
/** /**
* Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map * Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
* that Auxio uses. * that Auxio uses.
*/ */
val String.id3GenreName: String fun String.parseId3GenreName() = parseId3v1Genre() ?: parseId3v2Genre() ?: this
get() = parseId3v1Genre() ?: parseId3v2Genre() ?: this
private fun String.parseId3v1Genre(): String? = private fun String.parseId3v1Genre(): String? =
when { when {
@ -158,7 +135,7 @@ private fun String.parseId3v2Genre(): String? {
// ID3v1 tags. // ID3v1 tags.
val genreIds = groups[1] val genreIds = groups[1]
if (genreIds != null && genreIds.value.isNotEmpty()) { 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) { for (id in ids) {
id.parseId3v1Genre()?.let(genres::add) id.parseId3v1Genre()?.let(genres::add)
} }

View file

@ -26,11 +26,10 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.id3GenreName import org.oxycblt.auxio.music.parseId3GenreName
import org.oxycblt.auxio.music.iso8601year import org.oxycblt.auxio.music.parseIso8601Year
import org.oxycblt.auxio.music.plainTrackNo import org.oxycblt.auxio.music.parseNum
import org.oxycblt.auxio.music.trackDiscNo import org.oxycblt.auxio.music.parsePositionNum
import org.oxycblt.auxio.music.year
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW 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 } tags["TSOT"]?.let { audio.sortTitle = it }
// Track, as NN/TT // Track, as NN/TT
tags["TRCK"]?.trackDiscNo?.let { audio.track = it } tags["TRCK"]?.parsePositionNum()?.let { audio.track = it }
// Disc, as NN/TT // 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 // 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
@ -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 // 3. ID3v2.4 Release Date, as it is the second most common date type
// 4. ID3v2.3 Original Date, as it is like #1 // 4. ID3v2.3 Original Date, as it is like #1
// 5. ID3v2.3 Release Year, as it is the most common date type // 5. ID3v2.3 Release Year, as it is the most common date type
(tags["TDOR"]?.iso8601year (tags["TDOR"]?.parseIso8601Year()
?: tags["TDRC"]?.iso8601year ?: tags["TDRL"]?.iso8601year ?: tags["TORY"]?.year ?: tags["TDRC"]?.parseIso8601Year() ?: tags["TDRL"]?.parseIso8601Year()
?: tags["TYER"]?.year) ?: tags["TORY"]?.parseNum() ?: tags["TYER"]?.parseNum())
?.let { audio.year = it } ?.let { audio.year = it }
// (Sort) Album // (Sort) Album
@ -243,7 +242,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
tags["TSO2"]?.let { audio.sortAlbumArtist = it } tags["TSO2"]?.let { audio.sortAlbumArtist = it }
// Genre, with the weird ID3 rules. // 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<String, String>) { private fun populateVorbis(tags: Map<String, String>) {
@ -252,10 +251,10 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
tags["TITLESORT"]?.let { audio.sortTitle = it } tags["TITLESORT"]?.let { audio.sortTitle = it }
// Track. Probably not NN/TT, as TOTALTRACKS handles totals. // 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. // 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 // Vorbis dates are less complicated, but there are still several types
// Our hierarchy for dates is as such: // 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 // 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 // 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!) // 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 } ?.let { audio.year = it }
// (Sort) Album // (Sort) Album
@ -274,7 +274,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
tags["ARTIST"]?.let { audio.artist = it } tags["ARTIST"]?.let { audio.artist = it }
tags["ARTISTSORT"]?.let { audio.sortArtist = it } tags["ARTISTSORT"]?.let { audio.sortArtist = it }
// (Sort) Album artist. // (Sort) Album artist
tags["ALBUMARTIST"]?.let { audio.albumArtist = it } tags["ALBUMARTIST"]?.let { audio.albumArtist = it }
tags["ALBUMARTISTSORT"]?.let { audio.sortAlbumArtist = it } tags["ALBUMARTISTSORT"]?.let { audio.sortAlbumArtist = it }

View file

@ -34,13 +34,13 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.albumCoverUri import org.oxycblt.auxio.music.albumCoverUri
import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.directoryCompat import org.oxycblt.auxio.music.directoryCompat
import org.oxycblt.auxio.music.id3GenreName
import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.packedDiscNo import org.oxycblt.auxio.music.parseId3GenreName
import org.oxycblt.auxio.music.packedTrackNo import org.oxycblt.auxio.music.parsePositionNum
import org.oxycblt.auxio.music.queryCursor import org.oxycblt.auxio.music.queryCursor
import org.oxycblt.auxio.music.storageVolumesCompat 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.music.useQuery
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe 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 // format a genre was derived from, we have to treat them like they are ID3
// genres, even when they might not be. // genres, even when they might not be.
val id = genreCursor.getLong(idIndex) val id = genreCursor.getLong(idIndex)
val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).id3GenreName val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).parseId3GenreName()
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
@ -357,17 +357,13 @@ abstract class MediaStoreBackend : Indexer.Backend {
rawSortName = sortTitle, rawSortName = sortTitle,
path = path =
Path( Path(
name = name = requireNotNull(displayName) { "Malformed audio: No display name" },
requireNotNull(displayName) { "Malformed audio: No display name" }, parent = requireNotNull(dir) { "Malformed audio: No parent directory" }),
parent =
requireNotNull(dir) { "Malformed audio: No parent directory" }),
uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri, uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri,
mimeType = mimeType =
MimeType( MimeType(
fromExtension = fromExtension =
requireNotNull(extensionMimeType) { requireNotNull(extensionMimeType) { "Malformed audio: No mime type" },
"Malformed audio: No mime type"
},
fromFormat = formatMimeType), fromFormat = formatMimeType),
size = requireNotNull(size) { "Malformed audio: No size" }, size = requireNotNull(size) { "Malformed audio: No size" },
dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" }, dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" },
@ -469,8 +465,8 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
val rawTrack = cursor.getIntOrNull(trackIndex) val rawTrack = cursor.getIntOrNull(trackIndex)
if (rawTrack != null) { if (rawTrack != null) {
rawTrack.packedTrackNo?.let { audio.track = it } rawTrack.unpackTrackNo()?.let { audio.track = it }
rawTrack.packedDiscNo?.let { audio.disc = it } rawTrack.unpackDiscNo()?.let { audio.disc = it }
} }
return audio return audio
@ -555,8 +551,8 @@ open class Api29MediaStoreBackend : BaseApi29MediaStoreBackend() {
// Use the old field instead. // Use the old field instead.
val rawTrack = cursor.getIntOrNull(trackIndex) val rawTrack = cursor.getIntOrNull(trackIndex)
if (rawTrack != null) { if (rawTrack != null) {
rawTrack.packedTrackNo?.let { audio.track = it } rawTrack.unpackTrackNo()?.let { audio.track = it }
rawTrack.packedDiscNo?.let { audio.disc = it } rawTrack.unpackDiscNo()?.let { audio.disc = it }
} }
return audio 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 // N is the number and T is the total. Parse the number while leaving out the
// total, as we have no use for it. // total, as we have no use for it.
cursor.getStringOrNull(trackIndex)?.trackDiscNo?.let { audio.track = it } cursor.getStringOrNull(trackIndex)?.parsePositionNum()?.let { audio.track = it }
cursor.getStringOrNull(discIndex)?.trackDiscNo?.let { audio.disc = it } cursor.getStringOrNull(discIndex)?.parsePositionNum()?.let { audio.disc = it }
return audio return audio
} }