music: make playlist uids random

Make playlist UIDs randomly generated.

This will allow multiple playlists with the same name, which may be
useful.
This commit is contained in:
Alexander Capehart 2023-03-23 17:15:38 -06:00
parent f388e492aa
commit 97b63992b5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 79 additions and 64 deletions

View file

@ -22,7 +22,6 @@ import android.content.Context
import android.net.Uri
import android.os.Parcelable
import androidx.room.TypeConverter
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
import java.util.UUID
@ -147,47 +146,13 @@ sealed interface Music : Item {
companion object {
/**
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
* unlikely-to-change metadata of the music.
* Creates an Auxio-style [UID] with a [UUID] generated by internal Auxio code.
*
* @param mode The analogous [MusicMode] of the item that created this [UID].
* @param updates Block to update the [MessageDigest] hash with the metadata of the
* item. Make sure the metadata hashed semantically aligns with the format
* specification.
* @param uuid The generated [UUID] for this item.
* @return A new auxio-style [UID].
*/
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
val digest =
MessageDigest.getInstance("SHA-256").run {
updates()
digest()
}
// Convert the digest to a UUID. This does cleave off some of the hash, but this
// is considered okay.
val uuid =
UUID(
digest[0]
.toLong()
.shl(56)
.or(digest[1].toLong().and(0xFF).shl(48))
.or(digest[2].toLong().and(0xFF).shl(40))
.or(digest[3].toLong().and(0xFF).shl(32))
.or(digest[4].toLong().and(0xFF).shl(24))
.or(digest[5].toLong().and(0xFF).shl(16))
.or(digest[6].toLong().and(0xFF).shl(8))
.or(digest[7].toLong().and(0xFF)),
digest[8]
.toLong()
.shl(56)
.or(digest[9].toLong().and(0xFF).shl(48))
.or(digest[10].toLong().and(0xFF).shl(40))
.or(digest[11].toLong().and(0xFF).shl(32))
.or(digest[12].toLong().and(0xFF).shl(24))
.or(digest[13].toLong().and(0xFF).shl(16))
.or(digest[14].toLong().and(0xFF).shl(8))
.or(digest[15].toLong().and(0xFF)))
return UID(Format.AUXIO, mode, uuid)
}
fun auxio(mode: MusicMode, uuid: UUID) = UID(Format.AUXIO, mode, uuid)
/**
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
@ -198,7 +163,7 @@ sealed interface Music : Item {
* file.
* @return A new MusicBrainz-style [UID].
*/
fun musicBrainz(mode: MusicMode, mbid: UUID): UID = UID(Format.MUSICBRAINZ, mode, mbid)
fun musicBrainz(mode: MusicMode, mbid: UUID) = UID(Format.MUSICBRAINZ, mode, mbid)
/**
* Convert a [UID]'s string representation back into a concrete [UID] instance.

View file

@ -32,8 +32,8 @@ import org.oxycblt.auxio.util.logD
* Organized music library information obtained from device storage.
*
* This class allows for the creation of a well-formed music library graph from raw song
* information. It's generally not expected to create this yourself and instead use
* [MusicRepository].
* information. Instances are immutable. It's generally not expected to create this yourself and
* instead use [MusicRepository].
*
* @author Alexander Capehart
*/

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.music.device
import android.content.Context
import androidx.annotation.VisibleForTesting
import java.security.MessageDigest
import java.util.*
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.*
@ -48,7 +49,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
?: Music.UID.auxio(MusicMode.SONGS) {
?: createHashedUid(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.
@ -231,7 +232,7 @@ class AlbumImpl(
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
?: Music.UID.auxio(MusicMode.ALBUMS) {
?: createHashedUid(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.
@ -330,7 +331,7 @@ class ArtistImpl(
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
?: createHashedUid(MusicMode.ARTISTS) { update(rawArtist.name) }
override val rawName = rawArtist.name
override val rawSortName = rawArtist.sortName
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) }
@ -415,7 +416,7 @@ class GenreImpl(
musicSettings: MusicSettings,
override val songs: List<SongImpl>
) : Genre {
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
override val uid = createHashedUid(MusicMode.GENRES) { update(rawGenre.name) }
override val rawName = rawGenre.name
override val rawSortName = rawName
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) }
@ -473,6 +474,48 @@ class GenreImpl(
}
}
/**
* Generate a [Music.UID] derived from the hash of objective music metadata.
*
* @param mode The analogous [MusicMode] of the item that created this [UID].
* @param updates Block to update the [MessageDigest] hash with the metadata of the item. Make sure
* the metadata hashed semantically aligns with the format specification.
* @return A new [Music.UID] of Auxio format whose [UUID] was derived from the SHA-256 hash of the
* metadata given.
*/
fun createHashedUid(mode: MusicMode, updates: MessageDigest.() -> Unit): Music.UID {
val digest =
MessageDigest.getInstance("SHA-256").run {
updates()
digest()
}
// Convert the digest to a UUID. This does cleave off some of the hash, but this
// is considered okay.
val uuid =
UUID(
digest[0]
.toLong()
.shl(56)
.or(digest[1].toLong().and(0xFF).shl(48))
.or(digest[2].toLong().and(0xFF).shl(40))
.or(digest[3].toLong().and(0xFF).shl(32))
.or(digest[4].toLong().and(0xFF).shl(24))
.or(digest[5].toLong().and(0xFF).shl(16))
.or(digest[6].toLong().and(0xFF).shl(8))
.or(digest[7].toLong().and(0xFF)),
digest[8]
.toLong()
.shl(56)
.or(digest[9].toLong().and(0xFF).shl(48))
.or(digest[10].toLong().and(0xFF).shl(40))
.or(digest[11].toLong().and(0xFF).shl(32))
.or(digest[12].toLong().and(0xFF).shl(24))
.or(digest[13].toLong().and(0xFF).shl(16))
.or(digest[14].toLong().and(0xFF).shl(8))
.or(digest[15].toLong().and(0xFF)))
return Music.UID.auxio(mode, uuid)
}
/**
* Update a [MessageDigest] with a lowercase [String].
*

View file

@ -19,20 +19,36 @@
package org.oxycblt.auxio.music.user
import android.content.Context
import java.util.*
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.device.DeviceLibrary
class PlaylistImpl(
rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary,
class PlaylistImpl
private constructor(
override val uid: Music.UID,
override val rawName: String,
override val songs: List<Song>,
musicSettings: MusicSettings
) : Playlist {
override val uid = rawPlaylist.playlistInfo.playlistUid
override val rawName = rawPlaylist.playlistInfo.name
constructor(
name: String,
songs: List<Song>,
musicSettings: MusicSettings
) : this(Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), name, songs, musicSettings)
constructor(
rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary,
musicSettings: MusicSettings
) : this(
rawPlaylist.playlistInfo.playlistUid,
rawPlaylist.playlistInfo.name,
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) },
musicSettings)
override fun resolveName(context: Context) = rawName
override val rawSortName = null
override val sortName = SortName(rawName, musicSettings)
override val songs = rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }
override val durationMs = songs.sumOf { it.durationMs }
override val albums =
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }

View file

@ -20,10 +20,7 @@ package org.oxycblt.auxio.music.user
import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.device.DeviceLibrary
/**
@ -54,6 +51,7 @@ interface UserLibrary {
*
* @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later.
* This allows database information to be read before the actual instance is constructed.
* @return A new [UserLibrary] with the required implementation.
*/
suspend fun read(deviceLibrary: Channel<DeviceLibrary>): UserLibrary
}
@ -77,15 +75,8 @@ private class UserLibraryImpl(
get() = playlistMap.values.toList()
init {
val uid = Music.UID.auxio(MusicMode.PLAYLISTS) { update("Playlist 1".toByteArray()) }
playlistMap[uid] =
PlaylistImpl(
RawPlaylist(
PlaylistInfo(uid, "Playlist 1"),
deviceLibrary.songs.slice(10..30).map { PlaylistSong(it.uid) }),
deviceLibrary,
musicSettings,
)
val playlist = PlaylistImpl("Playlist 1", deviceLibrary.songs.slice(58..100), musicSettings)
playlistMap[playlist.uid] = playlist
}
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]