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.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.room.TypeConverter import androidx.room.TypeConverter
import java.security.MessageDigest
import java.text.CollationKey import java.text.CollationKey
import java.text.Collator import java.text.Collator
import java.util.UUID import java.util.UUID
@ -147,47 +146,13 @@ sealed interface Music : Item {
companion object { companion object {
/** /**
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective, * Creates an Auxio-style [UID] with a [UUID] generated by internal Auxio code.
* unlikely-to-change metadata of the music.
* *
* @param mode The analogous [MusicMode] of the item that created this [UID]. * @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 * @param uuid The generated [UUID] for this item.
* item. Make sure the metadata hashed semantically aligns with the format
* specification.
* @return A new auxio-style [UID]. * @return A new auxio-style [UID].
*/ */
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID { fun auxio(mode: MusicMode, uuid: UUID) = UID(Format.AUXIO, mode, uuid)
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)
}
/** /**
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID * Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
@ -198,7 +163,7 @@ sealed interface Music : Item {
* file. * file.
* @return A new MusicBrainz-style [UID]. * @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. * 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. * Organized music library information obtained from device storage.
* *
* This class allows for the creation of a well-formed music library graph from raw song * 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 * information. Instances are immutable. It's generally not expected to create this yourself and
* [MusicRepository]. * instead use [MusicRepository].
* *
* @author Alexander Capehart * @author Alexander Capehart
*/ */

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.music.device
import android.content.Context import android.content.Context
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import java.security.MessageDigest import java.security.MessageDigest
import java.util.*
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
@ -48,7 +49,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed 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) } 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 // 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 // consistent across music setting changes. Parents are not held up to the
// same standard since grouping is already inherently linked to settings. // same standard since grouping is already inherently linked to settings.
@ -231,7 +232,7 @@ class AlbumImpl(
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) } 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. // 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 // 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. // the exact same name, but if there is, I would love to know.
@ -330,7 +331,7 @@ class ArtistImpl(
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } 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 rawName = rawArtist.name
override val rawSortName = rawArtist.sortName override val rawSortName = rawArtist.sortName
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) }
@ -415,7 +416,7 @@ class GenreImpl(
musicSettings: MusicSettings, musicSettings: MusicSettings,
override val songs: List<SongImpl> override val songs: List<SongImpl>
) : Genre { ) : 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 rawName = rawGenre.name
override val rawSortName = rawName override val rawSortName = rawName
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } 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]. * Update a [MessageDigest] with a lowercase [String].
* *

View file

@ -19,20 +19,36 @@
package org.oxycblt.auxio.music.user package org.oxycblt.auxio.music.user
import android.content.Context import android.content.Context
import java.util.*
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
class PlaylistImpl( class PlaylistImpl
private constructor(
override val uid: Music.UID,
override val rawName: String,
override val songs: List<Song>,
musicSettings: MusicSettings
) : Playlist {
constructor(
name: String,
songs: List<Song>,
musicSettings: MusicSettings
) : this(Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), name, songs, musicSettings)
constructor(
rawPlaylist: RawPlaylist, rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary, deviceLibrary: DeviceLibrary,
musicSettings: MusicSettings musicSettings: MusicSettings
) : Playlist { ) : this(
override val uid = rawPlaylist.playlistInfo.playlistUid rawPlaylist.playlistInfo.playlistUid,
override val rawName = rawPlaylist.playlistInfo.name rawPlaylist.playlistInfo.name,
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) },
musicSettings)
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
override val rawSortName = null override val rawSortName = null
override val sortName = SortName(rawName, musicSettings) 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 durationMs = songs.sumOf { it.durationMs }
override val albums = override val albums =
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } 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 javax.inject.Inject
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
/** /**
@ -54,6 +51,7 @@ interface UserLibrary {
* *
* @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later. * @param deviceLibrary Asynchronously populated [DeviceLibrary] that can be obtained later.
* This allows database information to be read before the actual instance is constructed. * 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 suspend fun read(deviceLibrary: Channel<DeviceLibrary>): UserLibrary
} }
@ -77,15 +75,8 @@ private class UserLibraryImpl(
get() = playlistMap.values.toList() get() = playlistMap.values.toList()
init { init {
val uid = Music.UID.auxio(MusicMode.PLAYLISTS) { update("Playlist 1".toByteArray()) } val playlist = PlaylistImpl("Playlist 1", deviceLibrary.songs.slice(58..100), musicSettings)
playlistMap[uid] = playlistMap[playlist.uid] = playlist
PlaylistImpl(
RawPlaylist(
PlaylistInfo(uid, "Playlist 1"),
deviceLibrary.songs.slice(10..30).map { PlaylistSong(it.uid) }),
deviceLibrary,
musicSettings,
)
} }
override fun findPlaylist(uid: Music.UID) = playlistMap[uid] override fun findPlaylist(uid: Music.UID) = playlistMap[uid]