From 969c0c69b7360c6c9748f22d27f64b0ccc5728a3 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 13 Jul 2022 20:16:22 -0600 Subject: [PATCH] music: add sort tag support [#172] Implement sort tag support in the ExoPlayer backend. Sort tags for grouping is still derived from the templates. Album artist sort tags are only picked if one is present. System might be a bit buggy at the moment given that it messes with grouping/sorting a little. Resolves #172. --- CHANGELOG.md | 3 +- .../java/org/oxycblt/auxio/music/Music.kt | 34 ++++++---- .../auxio/music/system/ExoPlayerBackend.kt | 28 ++++---- .../org/oxycblt/auxio/music/system/Indexer.kt | 12 +++- .../auxio/music/system/MediaStoreBackend.kt | 64 +++++++++++-------- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 90 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cddc09e1..d0f673813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog -## 2.6.0 +## dev #### 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 Last Added sorting ## 2.5.0 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 83db4421c..227abaa45 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -69,6 +69,7 @@ sealed class MusicParent : Music() { /** The data object for a song. */ data class Song( override val rawName: String, + override val rawSortName: String?, /** The path of this song. */ val path: Path, /** The URI linking to this song's file. */ @@ -90,12 +91,18 @@ data class Song( /** Internal field. Do not use. */ val _albumName: String, /** Internal field. Do not use. */ + val _albumSortName: String?, + /** Internal field. Do not use. */ val _albumCoverUri: Uri, /** Internal field. Do not use. */ val _artistName: String?, /** Internal field. Do not use. */ + val _artistSortName: String?, + /** Internal field. Do not use. */ val _albumArtistName: String?, /** Internal field. Do not use. */ + val _albumArtistSortName: String?, + /** Internal field. Do not use. */ val _genreName: String? ) : Music() { override val id: Long @@ -109,9 +116,6 @@ data class Song( return result } - override val rawSortName: String? - get() = null - override fun resolveName(context: Context) = rawName /** The duration of this song, in seconds (rounded down) */ @@ -159,6 +163,14 @@ data class Song( val _artistGroupingName: String? get() = _albumArtistName ?: _artistName + /** Internal field. Do not use. */ + val _artistGroupingSortName: String? + get() = + // Only use the album artist sort name if we have one, otherwise ignore it. + _albumArtistName?.let { _albumArtistSortName } ?: _artistName?.let { _artistSortName } + + /** Internal field. Do not use. */ + /** Internal field. Do not use. */ val _isMissingAlbum: Boolean get() = _album == null @@ -183,6 +195,7 @@ data class Song( /** The data object for an album. */ data class Album( override val rawName: String, + override val rawSortName: String?, /** The latest year of the songs in this album. Null if none of the songs had metadata. */ val year: Int?, /** The URI for the cover art corresponding to this album. */ @@ -191,6 +204,8 @@ data class Album( override val songs: List, /** Internal field. Do not use. */ val _artistGroupingName: String?, + /** Internal field. Do not use. */ + val _artistGroupingSortName: String? ) : MusicParent() { init { for (song in songs) { @@ -206,9 +221,6 @@ data class Album( return result } - override val rawSortName: String? - get() = null - override fun resolveName(context: Context) = rawName private var _artist: Artist? = null @@ -236,6 +248,7 @@ data class Album( */ data class Artist( override val rawName: String?, + override val rawSortName: String?, /** The albums of this artist. */ val albums: List ) : MusicParent() { @@ -248,9 +261,6 @@ data class Artist( override val id: Long get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong() - override val rawSortName: String? - get() = null - override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist) /** The songs of this artist. */ @@ -265,11 +275,11 @@ data class Genre(override val rawName: String?, override val songs: List) } } - override val id: Long - get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong() - override val rawSortName: String? get() = null + override val id: Long + get() = (rawName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong() + override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) } 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 f0da4f46e..bd15f5e40 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 @@ -206,8 +206,9 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { } private fun populateId3v2(tags: Map) { - // Title + // (Sort) Title tags["TIT2"]?.let { audio.title = it } + tags["TSOT"]?.let { audio.sortTitle = it } // Track, as NN/TT tags["TRCK"]?.trackDiscNo?.let { audio.track = it } @@ -229,22 +230,26 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { ?: tags["TYER"]?.year) ?.let { audio.year = it } - // Album + // (Sort) Album tags["TALB"]?.let { audio.album = it } + tags["TSOA"]?.let { audio.sortAlbum = it } - // Artist + // (Sort) Artist tags["TPE1"]?.let { audio.artist = it } + tags["TSOP"]?.let { audio.sortArtist = it } - // Album artist + // (Sort) Album artist tags["TPE2"]?.let { audio.albumArtist = it } + tags["TSO2"]?.let { audio.sortAlbumArtist = it } // Genre, with the weird ID3 rules. tags["TCON"]?.let { audio.genre = it.id3GenreName } } private fun populateVorbis(tags: Map) { - // Title + // (Sort) Title tags["TITLE"]?.let { audio.title = it } + tags["TITLESORT"]?.let { audio.sortTitle = it } // Track. Probably not NN/TT, as TOTALTRACKS handles totals. tags["TRACKNUMBER"]?.plainTrackNo?.let { audio.track = it } @@ -261,16 +266,17 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { (tags["ORIGINALDATE"]?.iso8601year ?: tags["DATE"]?.iso8601year ?: tags["YEAR"]?.year) ?.let { audio.year = it } - // Album + // (Sort) Album tags["ALBUM"]?.let { audio.album = it } + tags["ALBUMSORT"]?.let { audio.sortAlbum = it } - // Artist + // (Sort) Artist tags["ARTIST"]?.let { audio.artist = it } + tags["ARTISTSORT"]?.let { audio.sortArtist = it } - // Album artist. This actually comes into two flavors: - // 1. ALBUMARTIST, which is the most common - // 2. ALBUM ARTIST, which is present on older vorbis tags - (tags["ALBUMARTIST"] ?: tags["ALBUM ARTIST"])?.let { audio.albumArtist = it } + // (Sort) Album artist. + tags["ALBUMARTIST"]?.let { audio.albumArtist = it } + tags["ALBUMARTISTSORT"]?.let { audio.sortAlbumArtist = it } // Genre, no ID3 rules here tags["GENRE"]?.let { audio.genre = it } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index ccdbf93a9..f18eeb299 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -313,10 +313,12 @@ class Indexer { albums.add( Album( rawName = templateSong._albumName, + rawSortName = templateSong._albumSortName, year = templateSong._year, albumCoverUri = templateSong._albumCoverUri, songs = entry.value, - _artistGroupingName = templateSong._artistGroupingName)) + _artistGroupingName = templateSong._artistGroupingName, + _artistGroupingSortName = templateSong._artistGroupingSortName)) } logD("Successfully built ${albums.size} albums") @@ -335,10 +337,14 @@ class Indexer { for (entry in albumsByArtist) { // The first album will suffice for template metadata. val templateAlbum = entry.value[0] - artists.add(Artist(rawName = templateAlbum._artistGroupingName, albums = entry.value)) + artists.add( + Artist( + rawName = templateAlbum._artistGroupingName, + rawSortName = templateAlbum._artistGroupingSortName, + albums = entry.value)) } - logD("Successfully built ${artists.size} artists") + `logD`("Successfully built ${artists.size} artists") return artists } 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 c2e9b1a51..08b7ff16e 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 @@ -238,6 +238,7 @@ abstract class MediaStoreBackend : Indexer.Backend { open val projection: Array get() = arrayOf( + // These columns are guaranteed to work on all versions of android MediaStore.Audio.AudioColumns._ID, MediaStore.Audio.AudioColumns.TITLE, MediaStore.Audio.AudioColumns.DISPLAY_NAME, @@ -327,6 +328,7 @@ abstract class MediaStoreBackend : Indexer.Backend { data class Audio( var id: Long? = null, var title: String? = null, + var sortTitle: String? = null, var displayName: String? = null, var dir: Directory? = null, var extensionMimeType: String? = null, @@ -338,38 +340,50 @@ abstract class MediaStoreBackend : Indexer.Backend { var disc: Int? = null, var year: Int? = null, var album: String? = null, + var sortAlbum: String? = null, var albumId: Long? = null, var artist: String? = null, + var sortArtist: String? = null, var albumArtist: String? = null, + var sortAlbumArtist: String? = null, var genre: String? = null ) { 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" }, - 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" }, - _albumCoverUri = - requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri, - _artistName = artist, - _albumArtistName = albumArtist, - _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 { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b1593d38..6e9f7c2c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,7 +150,7 @@ Include Music will only be loaded from the folders you add. Ignore MediaStore tags - Increases tag quality, but requires longer loading times (Experimental) + Increases tag quality, but results in longer loading times (Experimental) Automatic reloading Reload your music library whenever it changes (Experimental)