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:
parent
f388e492aa
commit
97b63992b5
5 changed files with 79 additions and 64 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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].
|
||||
*
|
||||
|
|
|
@ -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(
|
||||
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,
|
||||
deviceLibrary: DeviceLibrary,
|
||||
musicSettings: MusicSettings
|
||||
) : Playlist {
|
||||
override val uid = rawPlaylist.playlistInfo.playlistUid
|
||||
override val rawName = rawPlaylist.playlistInfo.name
|
||||
) : 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 }
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue