music: add basic multi-genre support [#201]
Add basic support for multiple genres. This is sort of the test run for full multi-artist support, allowing me to rework my abstractions to handle the presence of multiple parents. This is nowhere near complete. For example, there is currently a stopgap measure in the playback system that basically breaks genre playback. It's a start though.
This commit is contained in:
parent
022f92f27f
commit
b24e22182e
8 changed files with 55 additions and 35 deletions
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
|
|||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.resolveYear
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
|
@ -111,12 +112,15 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
|
||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||
// the most "Prominent" genre.
|
||||
var genresByAmount = mutableMapOf<Genre, Int>()
|
||||
for (song in item.songs) {
|
||||
for (genre in song.genres) {
|
||||
genresByAmount[genre] = genresByAmount[genre]?.inc() ?: 1
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailSubhead.text =
|
||||
item.songs
|
||||
.groupBy { it.genre.resolveName(binding.context) }
|
||||
.entries
|
||||
.maxByOrNull { it.value.size }
|
||||
?.key
|
||||
genresByAmount.maxByOrNull { it.value }?.key?.resolveName(binding.context)
|
||||
?: binding.context.getString(R.string.def_genre)
|
||||
|
||||
binding.detailInfo.text =
|
||||
|
|
|
@ -123,10 +123,10 @@ data class Song(private val raw: Raw) : Music() {
|
|||
val album: Album
|
||||
get() = unlikelyToBeNull(_album)
|
||||
|
||||
private var _genre: Genre? = null
|
||||
private var _genres: MutableList<Genre> = mutableListOf()
|
||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||
val genre: Genre
|
||||
get() = unlikelyToBeNull(_genre)
|
||||
val genres: List<Genre>
|
||||
get() = _genres
|
||||
|
||||
/**
|
||||
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
||||
|
@ -149,14 +149,14 @@ data class Song(private val raw: Raw) : Music() {
|
|||
raw.albumName to
|
||||
raw.artistName to
|
||||
raw.albumArtistName to
|
||||
raw.genreName to
|
||||
raw.genreNames to
|
||||
track to
|
||||
disc to
|
||||
durationMs
|
||||
|
||||
val _rawAlbum: Album.Raw
|
||||
|
||||
val _rawGenre = Genre.Raw(raw.genreName)
|
||||
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
||||
|
||||
val _isMissingAlbum: Boolean
|
||||
get() = _album == null
|
||||
|
@ -165,14 +165,14 @@ data class Song(private val raw: Raw) : Music() {
|
|||
get() = _album?._isMissingArtist ?: true
|
||||
|
||||
val _isMissingGenre: Boolean
|
||||
get() = _genre == null
|
||||
get() = _genres.isEmpty()
|
||||
|
||||
fun _link(album: Album) {
|
||||
_album = album
|
||||
}
|
||||
|
||||
fun _link(genre: Genre) {
|
||||
_genre = genre
|
||||
_genres.add(genre)
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -220,7 +220,7 @@ data class Song(private val raw: Raw) : Music() {
|
|||
var artistSortName: String? = null,
|
||||
var albumArtistName: String? = null,
|
||||
var albumArtistSortName: String? = null,
|
||||
var genreName: String? = null
|
||||
var genreNames: List<String>? = null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -358,7 +358,16 @@ data class Genre(private val raw: Raw, override val songs: List<Song>) : MusicPa
|
|||
val durationMs = songs.sumOf { it.durationMs }
|
||||
|
||||
data class Raw(val name: String?) {
|
||||
val groupingId: Long = name.toMusicId()
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Raw) return false
|
||||
return when {
|
||||
name != null && other.name != null -> name.equals(other.name, true)
|
||||
name == null && other.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = name?.lowercase().hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,7 +379,7 @@ private fun String?.toMusicId(): Long {
|
|||
|
||||
var result = 0L
|
||||
for (ch in lowercase()) {
|
||||
result = 31 * result + ch.code
|
||||
result = 31 * result + ch.lowercaseChar().code
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -110,7 +110,8 @@ fun List<String>.parseReleaseType() = ReleaseType.parse(this)
|
|||
* Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
|
||||
* that Auxio uses.
|
||||
*/
|
||||
fun String.parseId3GenreName() = parseId3v1Genre() ?: parseId3v2Genre() ?: this
|
||||
fun String.parseId3GenreName() =
|
||||
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: listOf(this)
|
||||
|
||||
private fun String.parseId3v1Genre(): String? =
|
||||
when {
|
||||
|
@ -126,7 +127,7 @@ private fun String.parseId3v1Genre(): String? =
|
|||
else -> null
|
||||
}
|
||||
|
||||
private fun String.parseId3v2Genre(): String? {
|
||||
private fun String.parseId3v2Genre(): List<String>? {
|
||||
val groups = (GENRE_RE.matchEntire(this) ?: return null).groupValues
|
||||
val genres = mutableSetOf<String>()
|
||||
|
||||
|
@ -155,7 +156,7 @@ private fun String.parseId3v2Genre(): String? {
|
|||
}
|
||||
}
|
||||
|
||||
return genres.joinToString(separator = ", ").ifEmpty { null }
|
||||
return genres.toList()
|
||||
}
|
||||
|
||||
/** Regex that implements matching for ID3v2's genre format. */
|
||||
|
|
|
@ -50,7 +50,7 @@ import org.oxycblt.auxio.util.logW
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||
private val runningTasks: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||
|
||||
// No need to implement our own query logic, as this backend is still reliant on
|
||||
// MediaStore.
|
||||
|
@ -78,19 +78,19 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
|||
// executor and thus will crash the app if an error occurs instead of bubbling
|
||||
// back up to Indexer.
|
||||
spin@ while (true) {
|
||||
for (i in runningTasks.indices) {
|
||||
val task = runningTasks[i]
|
||||
for (i in taskPool.indices) {
|
||||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val song = task.get()
|
||||
if (song != null) {
|
||||
songs.add(song)
|
||||
emitIndexing(Indexer.Indexing.Songs(songs.size, total))
|
||||
runningTasks[i] = Task(context, raw)
|
||||
taskPool[i] = Task(context, raw)
|
||||
break@spin
|
||||
}
|
||||
} else {
|
||||
runningTasks[i] = Task(context, raw)
|
||||
taskPool[i] = Task(context, raw)
|
||||
break@spin
|
||||
}
|
||||
}
|
||||
|
@ -99,14 +99,14 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
|||
|
||||
spin@ while (true) {
|
||||
// Spin until all of the remaining tasks are complete.
|
||||
for (i in runningTasks.indices) {
|
||||
val task = runningTasks[i]
|
||||
for (i in taskPool.indices) {
|
||||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val song = task.get() ?: continue@spin
|
||||
songs.add(song)
|
||||
emitIndexing(Indexer.Indexing.Songs(songs.size, total))
|
||||
runningTasks[i] = null
|
||||
taskPool[i] = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["TSO2"]?.let { raw.albumArtistSortName = it }
|
||||
|
||||
// Genre, with the weird ID3 rules.
|
||||
tags["TCON"]?.let { raw.genreName = it.parseId3GenreName() }
|
||||
tags["TCON"]?.let { raw.genreNames = it.parseId3GenreName() }
|
||||
|
||||
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let {
|
||||
|
@ -313,7 +313,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortName = it.joinToString() }
|
||||
|
||||
// Genre, no ID3 rules here
|
||||
tags["GENRE"]?.let { raw.genreName = it.joinToString() }
|
||||
tags["GENRE"]?.let { raw.genreNames = it }
|
||||
|
||||
// Release type
|
||||
tags["RELEASETYPE"]?.parseReleaseType()?.let { raw.albumReleaseType = it }
|
||||
|
|
|
@ -332,12 +332,15 @@ class Indexer {
|
|||
*/
|
||||
private fun buildGenres(songs: List<Song>): List<Genre> {
|
||||
val genres = mutableListOf<Genre>()
|
||||
val songsByGenre = songs.groupBy { it._rawGenre.groupingId }
|
||||
val songsByGenre = mutableMapOf<Genre.Raw, MutableList<Song>>()
|
||||
for (song in songs) {
|
||||
for (rawGenre in song._rawGenres) {
|
||||
songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
|
||||
}
|
||||
}
|
||||
|
||||
for (entry in songsByGenre) {
|
||||
// The first song fill suffice for template metadata.
|
||||
val templateSong = entry.value[0]
|
||||
genres.add(Genre(templateSong._rawGenre, songs = entry.value))
|
||||
genres.add(Genre(entry.key, songs = entry.value))
|
||||
}
|
||||
|
||||
logD("Successfully built ${genres.size} genres")
|
||||
|
|
|
@ -201,7 +201,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
val songId = cursor.getLong(songIdIndex)
|
||||
rawSongs
|
||||
.find { it.mediaStoreId == songId }
|
||||
?.let { song -> song.genreName = name }
|
||||
?.let { song -> song.genreNames = name }
|
||||
|
||||
if (cursor.position % 50 == 0) {
|
||||
// Only check for a cancellation every 50 songs or so (~20ms).
|
||||
|
|
|
@ -162,7 +162,8 @@ class PlaybackStateManager private constructor() {
|
|||
PlaybackMode.ALL_SONGS -> null
|
||||
PlaybackMode.IN_ALBUM -> song.album
|
||||
PlaybackMode.IN_ARTIST -> song.album.artist
|
||||
PlaybackMode.IN_GENRE -> song.genre
|
||||
PlaybackMode.IN_GENRE ->
|
||||
song.genres.maxBy { it.songs.size } // TODO: Stopgap measure until I can rework this and add selection
|
||||
}
|
||||
|
||||
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)
|
||||
|
|
|
@ -160,7 +160,9 @@ class MediaSessionComponent(
|
|||
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
|
||||
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
|
||||
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
|
||||
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context))
|
||||
.putText(
|
||||
MediaMetadataCompat.METADATA_KEY_GENRE,
|
||||
song.genres.joinToString { it.resolveName(context) })
|
||||
.putText(
|
||||
METADATA_KEY_PARENT,
|
||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
||||
|
|
Loading…
Reference in a new issue