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.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
}

View file

@ -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) }

View file

@ -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<Song>,
/** Internal field. Do not use. */
@ -246,6 +254,12 @@ data class Album(
fun _link(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)
/**
* 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.

View file

@ -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<String, String>): 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 }
}
/**

View file

@ -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<Song>): List<Album> {
val albums = mutableListOf<Album>()
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))

View file

@ -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,