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:
parent
f0d6e13b53
commit
969c0c69b7
6 changed files with 90 additions and 53 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,25 +340,34 @@ 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
|
||||
// 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" }),
|
||||
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" },
|
||||
requireNotNull(extensionMimeType) {
|
||||
"Malformed audio: No mime type"
|
||||
},
|
||||
fromFormat = formatMimeType),
|
||||
size = requireNotNull(size) { "Malformed audio: No size" },
|
||||
dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" },
|
||||
|
@ -365,10 +376,13 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue