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.
This commit is contained in:
OxygenCobalt 2022-07-18 10:54:08 -06:00
parent f838fc8b0e
commit 5df8edf912
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 60 additions and 17 deletions

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.detail
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.text.method.MovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import com.google.android.material.textfield.TextInputEditText 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 * 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 * blatant abuse of Material Design Guidelines, but I also don't want to figure out how to plain
* plain text selectable. * text selectable.
* *
* @author OxygenCobalt * @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
} }

View file

@ -74,7 +74,7 @@ abstract class BaseFetcher : Fetcher {
fetchMediaStoreCovers(context, album) fetchMediaStoreCovers(context, album)
} }
} catch (e: Exception) { } 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 null
} }
} }
@ -203,7 +203,7 @@ abstract class BaseFetcher : Fetcher {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? { 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 // Eliminate any chance that this blocking call might mess up the loading process
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }

View file

@ -96,6 +96,8 @@ data class Song(
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _albumSortName: String?, val _albumSortName: String?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _albumType: Album.Type,
/** Internal field. Do not use. */
val _albumCoverUri: Uri, val _albumCoverUri: Uri,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _artistName: String?, val _artistName: String?,
@ -202,9 +204,15 @@ data class Song(
data class Album( data class Album(
override val rawName: String, override val rawName: String,
override val rawSortName: String?, override val rawSortName: String?,
/** The date this album was released. */
val date: Date?, 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. */ /** The songs of this album. */
override val songs: List<Song>, override val songs: List<Song>,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
@ -246,6 +254,12 @@ data class Album(
fun _link(artist: Artist) { fun _link(artist: Artist) {
_artist = artist _artist = artist
} }
enum class Type {
Album,
EP,
Single
}
} }
/** /**

View file

@ -45,7 +45,7 @@ fun <R> ContentResolver.useQuery(
): R? = queryCursor(uri, projection, selector, args)?.use(block) ): 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. * works since at least API 21.
*/ */
private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart") private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart")
@ -100,6 +100,17 @@ fun String.parseSortName() =
else -> this 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 * Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
* that Auxio uses. * that Auxio uses.

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.parseId3GenreName import org.oxycblt.auxio.music.parseId3GenreName
import org.oxycblt.auxio.music.parsePositionNum import org.oxycblt.auxio.music.parsePositionNum
import org.oxycblt.auxio.music.parseReleaseType
import org.oxycblt.auxio.music.parseTimestamp import org.oxycblt.auxio.music.parseTimestamp
import org.oxycblt.auxio.music.parseYear import org.oxycblt.auxio.music.parseYear
import org.oxycblt.auxio.util.logD 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()) { for (i in 0 until metadata.length()) {
when (val tag = metadata[i]) { when (val tag = metadata[i]) {
is TextInformationFrame -> { is TextInformationFrame -> {
val id = tag.id.sanitize() val id = tag.description?.let { "TXXX:${it.sanitize()}" } ?: tag.id.sanitize()
val value = tag.value.sanitize() val value = tag.value.sanitize()
if (value.isNotEmpty()) { if (value.isNotEmpty()) {
id3v2Tags[id] = value id3v2Tags[id] = value
@ -245,6 +246,11 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
// Genre, with the weird ID3 rules. // Genre, with the weird ID3 rules.
tags["TCON"]?.let { audio.genre = it.parseId3GenreName() } 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<String, String>): Date? { private fun parseId3v23Date(tags: Map<String, String>): Date? {
@ -303,6 +309,9 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
// Genre, no ID3 rules here // Genre, no ID3 rules here
tags["GENRE"]?.let { audio.genre = it } tags["GENRE"]?.let { audio.genre = it }
// Release type
tags["RELEASETYPE"]?.parseReleaseType()?.let { audio.albumType = it }
} }
/** /**

View file

@ -224,7 +224,6 @@ class Indexer {
val albums = buildAlbums(songs) val albums = buildAlbums(songs)
val artists = buildArtists(albums) val artists = buildArtists(albums)
val genres = buildGenres(songs) val genres = buildGenres(songs)
// Sanity check: Ensure that all songs are linked up to albums/artists/genres. // 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. * 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 * 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, * result in an unrelated album cover being selected depending on the song chosen as the
* but it seems to work pretty well. * template, but it seems to work pretty well.
*/ */
private fun buildAlbums(songs: List<Song>): List<Album> { private fun buildAlbums(songs: List<Song>): List<Album> {
val albums = mutableListOf<Album>() val albums = mutableListOf<Album>()
val songsByAlbum = songs.groupBy { it._albumGroupingId } 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) { for (entry in songsByAlbum) {
val albumSongs = entry.value val albumSongs = entry.value
@ -315,7 +321,8 @@ class Indexer {
rawName = templateSong._albumName, rawName = templateSong._albumName,
rawSortName = templateSong._albumSortName, rawSortName = templateSong._albumSortName,
date = templateSong._date, date = templateSong._date,
albumCoverUri = templateSong._albumCoverUri, type = if (enableAlbumTypes) templateSong._albumType else null,
coverUri = templateSong._albumCoverUri,
songs = entry.value, songs = entry.value,
_artistGroupingName = templateSong._artistGroupingName, _artistGroupingName = templateSong._artistGroupingName,
_artistGroupingSortName = templateSong._artistGroupingSortName)) _artistGroupingSortName = templateSong._artistGroupingSortName))

View file

@ -27,6 +27,7 @@ import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import java.io.File import java.io.File
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.MimeType import org.oxycblt.auxio.music.MimeType
@ -336,9 +337,10 @@ abstract class MediaStoreBackend : Indexer.Backend {
var track: Int? = null, var track: Int? = null,
var disc: Int? = null, var disc: Int? = null,
var date: Date? = null, var date: Date? = null,
var albumId: Long? = null,
var album: String? = null, var album: String? = null,
var sortAlbum: String? = null, var sortAlbum: String? = null,
var albumId: Long? = null, var albumType: Album.Type? = null,
var artist: String? = null, var artist: String? = null,
var sortArtist: String? = null, var sortArtist: String? = null,
var albumArtist: String? = null, var albumArtist: String? = null,
@ -369,6 +371,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
_date = date, _date = date,
_albumName = requireNotNull(album) { "Malformed audio: No album name" }, _albumName = requireNotNull(album) { "Malformed audio: No album name" },
_albumSortName = sortAlbum, _albumSortName = sortAlbum,
_albumType = albumType ?: Album.Type.Album,
_albumCoverUri = _albumCoverUri =
requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri, requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri,
_artistName = artist, _artistName = artist,