music: add musicbrainz id support
Add support for MusicBrainz IDs (MBIDs) in both grouping and UID creation. This should help with very large libraries where artist names collide, thus requiring differentiation through other means. It also theoretically opens the door to fetch online metadata, however I don't really care for that and it would violate the non-connectivity promise of Auxio. Resolves #202.
This commit is contained in:
parent
5c76838f69
commit
b58fce9414
4 changed files with 105 additions and 63 deletions
|
@ -4,10 +4,11 @@
|
|||
|
||||
#### What's New
|
||||
- Massively reworked music loading system:
|
||||
- Auxio now supports multiple artists
|
||||
- Auxio now supports multiple genres
|
||||
- Artists and album artists are now both given equal importance in the UI
|
||||
- Added support for multiple artists
|
||||
- Added support for multiple genres
|
||||
- Artists and album artists are now both given UI entires
|
||||
- Made music hashing rely on the more reliable MD5
|
||||
- Added support for MusicBrainz IDs (MBIDs)
|
||||
- **This may impact your library.** Instructions on how to update your library to result in a good
|
||||
artist experience will be added to the FAQ.
|
||||
|
||||
|
|
|
@ -160,17 +160,15 @@ sealed class Music : Item {
|
|||
}
|
||||
|
||||
val mode = MusicMode.fromInt(ids[0].toIntOrNull(16) ?: return null) ?: return null
|
||||
val uuid = UUID.fromString(ids[1])
|
||||
val uuid = ids[1].toUuidOrNull() ?: return null
|
||||
|
||||
return UID(format, mode, uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a UUID derived from the MD5 hash of the data digested in [updates].
|
||||
*
|
||||
* This is Auxio's UID format.
|
||||
*/
|
||||
fun hashed(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
||||
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
||||
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
||||
// tags in a music item. For easier use with MusicBrainz IDs, we transform
|
||||
// this into a UUID too.
|
||||
|
@ -179,6 +177,12 @@ sealed class Music : Item {
|
|||
val uuid = digest.digest().toUuid()
|
||||
return UID(Format.AUXIO, mode, uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a UUID derived from a MusicBrainz ID.
|
||||
*/
|
||||
fun musicBrainz(mode: MusicMode, uuid: UUID): UID =
|
||||
UID(Format.MUSICBRAINZ, mode, uuid)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,7 +207,7 @@ sealed class MusicParent : Music() {
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||
override val uid = UID.hashed(MusicMode.SONGS) {
|
||||
override val uid = raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } ?: UID.auxio(MusicMode.SONGS) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
// consistent across music setting changes. Parents are not held up to the
|
||||
// same standard since grouping is already inherently linked to settings.
|
||||
|
@ -273,20 +277,24 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
val album: Album
|
||||
get() = unlikelyToBeNull(_album)
|
||||
|
||||
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings)
|
||||
|
||||
private val artistNames = raw.artistNames.parseMultiValue(settings)
|
||||
|
||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
|
||||
|
||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings)
|
||||
|
||||
private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings)
|
||||
|
||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
|
||||
|
||||
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
|
||||
|
||||
private val rawArtists = artistNames.mapIndexed { i, name ->
|
||||
Artist.Raw(name, artistSortNames.getOrNull(i))
|
||||
Artist.Raw(artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, artistSortNames.getOrNull(i))
|
||||
}
|
||||
|
||||
private val rawAlbumArtists = albumArtistNames.mapIndexed { i, name ->
|
||||
Artist.Raw(name, albumArtistSortNames.getOrNull(i))
|
||||
Artist.Raw(albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, albumArtistSortNames.getOrNull(i))
|
||||
}
|
||||
|
||||
private val _artists = mutableListOf<Artist>()
|
||||
|
@ -339,15 +347,16 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
// --- INTERNAL FIELDS ---
|
||||
|
||||
val _rawGenres = raw.genreNames.parseId3GenreNames(settings)
|
||||
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw(null)) }
|
||||
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw()) }
|
||||
|
||||
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty {
|
||||
listOf(Artist.Raw(null, null))
|
||||
listOf(Artist.Raw())
|
||||
}
|
||||
|
||||
val _rawAlbum =
|
||||
Album.Raw(
|
||||
mediaStoreId = requireNotNull(raw.albumMediaStoreId) { "Invalid raw: No album id" },
|
||||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||
sortName = raw.albumSortName,
|
||||
releaseType = raw.albumReleaseType.parseReleaseType(settings),
|
||||
|
@ -377,7 +386,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
class Raw
|
||||
constructor(
|
||||
var mediaStoreId: Long? = null,
|
||||
var mbid: UUID? = null,
|
||||
var musicBrainzId: String? = null,
|
||||
var name: String? = null,
|
||||
var sortName: String? = null,
|
||||
var displayName: String? = null,
|
||||
|
@ -392,14 +401,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
var disc: Int? = null,
|
||||
var date: Date? = null,
|
||||
var albumMediaStoreId: Long? = null,
|
||||
var albumMbid: UUID? = null,
|
||||
var albumMusicBrainzId: String? = null,
|
||||
var albumName: String? = null,
|
||||
var albumSortName: String? = null,
|
||||
var albumReleaseType: List<String> = listOf(),
|
||||
var artistMbids: List<UUID> = listOf(),
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
var artistNames: List<String> = listOf(),
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
var albumArtistMbids: List<UUID> = listOf(),
|
||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
var albumArtistNames: List<String> = listOf(),
|
||||
var albumArtistSortNames: List<String> = listOf(),
|
||||
var genreNames: List<String> = listOf()
|
||||
|
@ -411,13 +420,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
override val uid = UID.hashed(MusicMode.ALBUMS) {
|
||||
// Hash based on only names despite the presence of a date to increase stability.
|
||||
// I don't know if there is any situation where an artist will have two albums with
|
||||
// the exact same name, but if there is, I would love to know.
|
||||
update(raw.name)
|
||||
update(raw.rawArtists.map { it.name })
|
||||
}
|
||||
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
|
||||
?: UID.auxio(MusicMode.ALBUMS) {
|
||||
// Hash based on only names despite the presence of a date to increase stability.
|
||||
// I don't know if there is any situation where an artist will have two albums with
|
||||
// the exact same name, but if there is, I would love to know.
|
||||
update(raw.name)
|
||||
update(raw.rawArtists.map { it.name })
|
||||
}
|
||||
|
||||
override val rawName = raw.name
|
||||
|
||||
|
@ -517,17 +527,27 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
|
||||
class Raw(
|
||||
val mediaStoreId: Long,
|
||||
val musicBrainzId: UUID?,
|
||||
val name: String,
|
||||
val sortName: String?,
|
||||
val releaseType: ReleaseType?,
|
||||
val rawArtists: List<Artist.Raw>
|
||||
) {
|
||||
private val hashCode = 31 * name.lowercase().hashCode() + rawArtists.hashCode()
|
||||
private val hashCode =
|
||||
musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Raw && name.equals(other.name, true) && rawArtists == other.rawArtists
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Raw) return false
|
||||
if (musicBrainzId != null && other.musicBrainzId != null &&
|
||||
musicBrainzId == other.musicBrainzId
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return name.equals(other.name, true) && rawArtists == other.rawArtists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -538,7 +558,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
*/
|
||||
class Artist
|
||||
constructor(raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||
override val uid = UID.hashed(MusicMode.ARTISTS) { update(raw.name) }
|
||||
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } ?: UID.auxio(MusicMode.ARTISTS) { update(raw.name) }
|
||||
|
||||
override val rawName = raw.name
|
||||
|
||||
|
@ -615,18 +635,26 @@ constructor(raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
|||
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
||||
}
|
||||
|
||||
class Raw(val name: String?, val sortName: String?) {
|
||||
private val hashCode = name?.lowercase().hashCode()
|
||||
class Raw(val musicBrainzId: UUID? = null, val name: String? = null, val sortName: String? = null) {
|
||||
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Raw &&
|
||||
when {
|
||||
name != null && other.name != null -> name.equals(other.name, true)
|
||||
name == null && other.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Raw) return false
|
||||
|
||||
if (musicBrainzId != null && other.musicBrainzId != null &&
|
||||
musicBrainzId == other.musicBrainzId
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return when {
|
||||
name != null && other.name != null -> name.equals(other.name, true)
|
||||
name == null && other.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,7 +663,7 @@ constructor(raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class Genre constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
override val uid = UID.hashed(MusicMode.GENRES) { update(raw.name) }
|
||||
override val uid = UID.auxio(MusicMode.GENRES) { update(raw.name) }
|
||||
|
||||
override val rawName = raw.name
|
||||
|
||||
|
@ -674,7 +702,7 @@ class Genre constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
check(songs.isNotEmpty()) { "Malformed genre: Empty" }
|
||||
}
|
||||
|
||||
class Raw(val name: String?) {
|
||||
class Raw(val name: String? = null) {
|
||||
private val hashCode = name?.lowercase().hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.provider.MediaStore
|
|||
import android.text.format.DateUtils
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.util.UUID
|
||||
|
||||
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
|
||||
fun ContentResolver.queryCursor(
|
||||
|
@ -58,6 +59,12 @@ val Long.audioUri: Uri
|
|||
val Long.albumCoverUri: Uri
|
||||
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
|
||||
|
||||
fun String.toUuidOrNull(): UUID? = try {
|
||||
UUID.fromString(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
|
||||
fun Date?.resolveYear(context: Context) =
|
||||
this?.resolveYear(context) ?: context.getString(R.string.def_date)
|
||||
|
|
|
@ -41,6 +41,8 @@ import org.oxycblt.auxio.util.logW
|
|||
* pitfalls given ExoPlayer's cozy relationship with native code. However, this backend should do
|
||||
* enough to eliminate such issues.
|
||||
*
|
||||
* TODO: Fix failing ID3v2 multi-value tests in fork (Implies parsing problem)
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MetadataExtractor(private val context: Context, private val mediaStoreExtractor: MediaStoreExtractor) {
|
||||
|
@ -193,7 +195,8 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
}
|
||||
|
||||
private fun populateId3v2(tags: Map<String, List<String>>) {
|
||||
// (Sort) Title
|
||||
// Song
|
||||
tags["TXXX:MusicBrainz Release Track Id"]?.let { raw.musicBrainzId = it[0] }
|
||||
tags["TIT2"]?.let { raw.name = it[0] }
|
||||
tags["TSOT"]?.let { raw.sortName = it[0] }
|
||||
|
||||
|
@ -219,25 +222,26 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
)
|
||||
?.let { raw.date = it }
|
||||
|
||||
// (Sort) Album
|
||||
// Album
|
||||
tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
tags["TALB"]?.let { raw.albumName = it[0] }
|
||||
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||
|
||||
// (Sort) Artist
|
||||
(tags["TXXX:ARTISTS"] ?: tags["TPE1"])?.let { raw.artistNames = it }
|
||||
tags["TSOP"]?.let { raw.artistSortNames = it }
|
||||
|
||||
// (Sort) Album artist
|
||||
tags["TPE2"]?.let { raw.albumArtistNames = it }
|
||||
tags["TSO2"]?.let { raw.albumArtistSortNames = it }
|
||||
|
||||
// Genre, with the weird ID3 rules.
|
||||
tags["TCON"]?.let { raw.genreNames = it }
|
||||
|
||||
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
|
||||
raw.albumReleaseType = it
|
||||
}
|
||||
|
||||
// Artist
|
||||
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
||||
(tags["TXXX:ARTISTS"] ?: tags["TPE1"])?.let { raw.artistNames = it }
|
||||
tags["TSOP"]?.let { raw.artistSortNames = it }
|
||||
|
||||
// Album artist
|
||||
tags["TXXX:MusicBrainz Album Artist Id"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||
tags["TPE2"]?.let { raw.albumArtistNames = it }
|
||||
tags["TSO2"]?.let { raw.albumArtistSortNames = it }
|
||||
|
||||
// Genre
|
||||
tags["TCON"]?.let { raw.genreNames = it }
|
||||
}
|
||||
|
||||
private fun parseId3v23Date(tags: Map<String, List<String>>): Date? {
|
||||
|
@ -267,7 +271,8 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
}
|
||||
|
||||
private fun populateVorbis(tags: Map<String, List<String>>) {
|
||||
// (Sort) Title
|
||||
// Song
|
||||
tags["MUSICBRAINZ_RELEASETRACKID"]?.let { raw.musicBrainzId = it[0] }
|
||||
tags["TITLE"]?.let { raw.name = it[0] }
|
||||
tags["TITLESORT"]?.let { raw.sortName = it[0] }
|
||||
|
||||
|
@ -290,23 +295,24 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
)
|
||||
?.let { raw.date = it }
|
||||
|
||||
// (Sort) Album
|
||||
// Album
|
||||
tags["MUSICBRAINZ_ALBUMID"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
tags["ALBUM"]?.let { raw.albumName = it[0] }
|
||||
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
|
||||
tags["RELEASETYPE"]?.let { raw.albumReleaseType = it }
|
||||
|
||||
// (Sort) Artist
|
||||
// Artist
|
||||
tags["MUSICBRAINZ_ARTISTID"]?.let { raw.artistMusicBrainzIds = it }
|
||||
tags["ARTIST"]?.let { raw.artistNames = it }
|
||||
tags["ARTISTSORT"]?.let { raw.artistSortNames = it }
|
||||
|
||||
// (Sort) Album artist
|
||||
// Album artist
|
||||
tags["MUSICBRAINZ_ALBUMARTISTID"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it }
|
||||
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it }
|
||||
|
||||
// Genre, no ID3 rules here
|
||||
// Genre
|
||||
tags["GENRE"]?.let { raw.genreNames = it }
|
||||
|
||||
// Release type
|
||||
tags["RELEASETYPE"]?.let { raw.albumReleaseType = it }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue