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.
This commit is contained in:
OxygenCobalt 2022-07-13 20:16:22 -06:00
parent f0d6e13b53
commit 969c0c69b7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 90 additions and 53 deletions

View file

@ -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

View file

@ -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<Song>,
/** 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<Album>
) : 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<Song>)
}
}
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)
}

View file

@ -206,8 +206,9 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
}
private fun populateId3v2(tags: Map<String, String>) {
// 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<String, String>) {
// 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 }

View file

@ -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
}

View file

@ -238,6 +238,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
open val projection: Array<String>
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 {

View file

@ -150,7 +150,7 @@
<string name="set_dirs_mode_include">Include</string>
<string name="set_dirs_mode_include_desc">Music will <b>only</b> be loaded from the folders you add.</string>
<string name="set_quality_tags">Ignore MediaStore tags</string>
<string name="set_quality_tags_desc">Increases tag quality, but requires longer loading times (Experimental)</string>
<string name="set_quality_tags_desc">Increases tag quality, but results in longer loading times (Experimental)</string>
<string name="set_observing">Automatic reloading</string>
<string name="set_observing_desc">Reload your music library whenever it changes (Experimental)</string>