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.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.
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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].
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
rawPlaylist: RawPlaylist,
|
private constructor(
|
||||||
deviceLibrary: DeviceLibrary,
|
override val uid: Music.UID,
|
||||||
|
override val rawName: String,
|
||||||
|
override val songs: List<Song>,
|
||||||
musicSettings: MusicSettings
|
musicSettings: MusicSettings
|
||||||
) : Playlist {
|
) : Playlist {
|
||||||
override val uid = rawPlaylist.playlistInfo.playlistUid
|
constructor(
|
||||||
override val rawName = rawPlaylist.playlistInfo.name
|
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 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 }
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue