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)