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,