diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index a764ef8ab..098aae9d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index a19aec5b4..404aa8f0c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -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 */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index f81644d10..88514313f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -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 ) : 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]. * diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index e5432e1d8..90a5da8f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -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, musicSettings: MusicSettings ) : Playlist { - override val uid = rawPlaylist.playlistInfo.playlistUid - override val rawName = rawPlaylist.playlistInfo.name + constructor( + name: String, + songs: List, + 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 593321c71..01fada793 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -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): 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]