From 5df8edf91230065b83f7b7750f3ba30534f2c9bd Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 18 Jul 2022 10:54:08 -0600 Subject: [PATCH] music: add basic release type support [#158] Add release type support to the media indexer. This allows EPs, Singles, and Albums to be distinguished from eachother. Auxio's implementation will use the MusicBrainz tag for ID3v2 (Falling back to GRP1, as that is used sometimes), and will use RELEASETYPE for vorbis comments. --- .../oxycblt/auxio/detail/ReadOnlyTextInput.kt | 11 +++++------ .../org/oxycblt/auxio/image/BaseFetcher.kt | 4 ++-- .../main/java/org/oxycblt/auxio/music/Music.kt | 18 ++++++++++++++++-- .../java/org/oxycblt/auxio/music/MusicUtil.kt | 13 ++++++++++++- .../auxio/music/system/ExoPlayerBackend.kt | 11 ++++++++++- .../org/oxycblt/auxio/music/system/Indexer.kt | 15 +++++++++++---- .../auxio/music/system/MediaStoreBackend.kt | 5 ++++- 7 files changed, 60 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt index 1c704c7c8..3a87850d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.detail import android.content.Context import android.os.Build -import android.text.method.MovementMethod import android.util.AttributeSet import android.view.View import com.google.android.material.textfield.TextInputEditText @@ -27,8 +26,8 @@ import org.oxycblt.auxio.R /** * A [TextInputEditText] that deliberately restricts all input except for selection. Yes, this is a - * blatant abuse of Material Design Guidelines, but I also don't want to figure out how to main - * plain text selectable. + * blatant abuse of Material Design Guidelines, but I also don't want to figure out how to plain + * text selectable. * * @author OxygenCobalt */ @@ -47,9 +46,9 @@ constructor( } } - override fun getFreezesText(): Boolean = false + override fun getFreezesText() = false - override fun getDefaultEditable(): Boolean = false + override fun getDefaultEditable() = false - override fun getDefaultMovementMethod(): MovementMethod? = null + override fun getDefaultMovementMethod() = null } diff --git a/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt index 6a020344f..f132f76cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt @@ -74,7 +74,7 @@ abstract class BaseFetcher : Fetcher { fetchMediaStoreCovers(context, album) } } catch (e: Exception) { - logW("Unable to extract album art due to an error: $e") + logW("Unable to extract album cover due to an error: $e") null } } @@ -203,7 +203,7 @@ abstract class BaseFetcher : Fetcher { @Suppress("BlockingMethodInNonBlockingContext") private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? { - val uri = data.albumCoverUri + val uri = data.coverUri // Eliminate any chance that this blocking call might mess up the loading process return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } 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 8b018ae6a..0d812bbcd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -96,6 +96,8 @@ data class Song( /** Internal field. Do not use. */ val _albumSortName: String?, /** Internal field. Do not use. */ + val _albumType: Album.Type, + /** Internal field. Do not use. */ val _albumCoverUri: Uri, /** Internal field. Do not use. */ val _artistName: String?, @@ -202,9 +204,15 @@ data class Song( data class Album( override val rawName: String, override val rawSortName: String?, + /** The date this album was released. */ val date: Date?, - /** The URI for the cover art corresponding to this album. */ - val albumCoverUri: Uri, + /** + * The type of release this album represents. Null if release types were not applicable to this + * library. + */ + val type: Type?, + /** The URI for the cover image corresponding to this album. */ + val coverUri: Uri, /** The songs of this album. */ override val songs: List, /** Internal field. Do not use. */ @@ -246,6 +254,12 @@ data class Album( fun _link(artist: Artist) { _artist = artist } + + enum class Type { + Album, + EP, + Single + } } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index ca52c9964..e3866633c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -45,7 +45,7 @@ fun ContentResolver.useQuery( ): R? = queryCursor(uri, projection, selector, args)?.use(block) /** - * For some reason the album art URI namespace does not have a member in [MediaStore], but it still + * For some reason the album cover URI namespace does not have a member in [MediaStore], but it still * works since at least API 21. */ private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart") @@ -100,6 +100,17 @@ fun String.parseSortName() = else -> this } +fun String.parseReleaseType() = + parseReleaseTypeImpl() ?: split("+", limit = 2)[0].trim().parseReleaseTypeImpl() + +private fun String.parseReleaseTypeImpl() = + when (this) { + "album" -> Album.Type.Album + "ep" -> Album.Type.EP + "single" -> Album.Type.Single + else -> null + } + /** * Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map * that Auxio uses. 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 03bd61a04..fe8e954d3 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 @@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.parseId3GenreName import org.oxycblt.auxio.music.parsePositionNum +import org.oxycblt.auxio.music.parseReleaseType import org.oxycblt.auxio.music.parseTimestamp import org.oxycblt.auxio.music.parseYear import org.oxycblt.auxio.util.logD @@ -177,7 +178,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { for (i in 0 until metadata.length()) { when (val tag = metadata[i]) { is TextInformationFrame -> { - val id = tag.id.sanitize() + val id = tag.description?.let { "TXXX:${it.sanitize()}" } ?: tag.id.sanitize() val value = tag.value.sanitize() if (value.isNotEmpty()) { id3v2Tags[id] = value @@ -245,6 +246,11 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { // Genre, with the weird ID3 rules. tags["TCON"]?.let { audio.genre = it.parseId3GenreName() } + + // Release type + (tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let { + audio.albumType = it + } } private fun parseId3v23Date(tags: Map): Date? { @@ -303,6 +309,9 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { // Genre, no ID3 rules here tags["GENRE"]?.let { audio.genre = it } + + // Release type + tags["RELEASETYPE"]?.parseReleaseType()?.let { audio.albumType = 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 93a68db0d..07c130033 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 @@ -224,7 +224,6 @@ class Indexer { val albums = buildAlbums(songs) val artists = buildArtists(albums) - val genres = buildGenres(songs) // Sanity check: Ensure that all songs are linked up to albums/artists/genres. @@ -294,13 +293,20 @@ class Indexer { * that all songs are unified under a single album. * * This does come with some costs, it's far slower than using the album ID itself, and it may - * result in an unrelated album art being selected depending on the song chosen as the template, - * but it seems to work pretty well. + * result in an unrelated album cover being selected depending on the song chosen as the + * template, but it seems to work pretty well. */ private fun buildAlbums(songs: List): List { val albums = mutableListOf() val songsByAlbum = songs.groupBy { it._albumGroupingId } + // If album types aren't used by the music library (Represented by all songs having + // an album type), there is no point in displaying them. + val enableAlbumTypes = songs.any { it._albumType != Album.Type.Album } + if (!enableAlbumTypes) { + logD("No distinct album types detected, ignoring them") + } + for (entry in songsByAlbum) { val albumSongs = entry.value @@ -315,7 +321,8 @@ class Indexer { rawName = templateSong._albumName, rawSortName = templateSong._albumSortName, date = templateSong._date, - albumCoverUri = templateSong._albumCoverUri, + type = if (enableAlbumTypes) templateSong._albumType else null, + coverUri = templateSong._albumCoverUri, songs = entry.value, _artistGroupingName = templateSong._artistGroupingName, _artistGroupingSortName = templateSong._artistGroupingSortName)) 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 7bd1bdf6e..74e0c632c 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 @@ -27,6 +27,7 @@ import androidx.annotation.RequiresApi import androidx.core.database.getIntOrNull import androidx.core.database.getStringOrNull import java.io.File +import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.MimeType @@ -336,9 +337,10 @@ abstract class MediaStoreBackend : Indexer.Backend { var track: Int? = null, var disc: Int? = null, var date: Date? = null, + var albumId: Long? = null, var album: String? = null, var sortAlbum: String? = null, - var albumId: Long? = null, + var albumType: Album.Type? = null, var artist: String? = null, var sortArtist: String? = null, var albumArtist: String? = null, @@ -369,6 +371,7 @@ abstract class MediaStoreBackend : Indexer.Backend { _date = date, _albumName = requireNotNull(album) { "Malformed audio: No album name" }, _albumSortName = sortAlbum, + _albumType = albumType ?: Album.Type.Album, _albumCoverUri = requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri, _artistName = artist,