music: rework id system

Completely rework the ID system to pave the way to MusicBrainz ID
support and greatly increase ID integrity in general.

This changeset removes the old ID field, an emulation of a polynomial
hash that was used in all items, and replaces it with a new type called
UID that is specific to Music. Other types just use plain equals now,
and most instances of "id" to check for equality in the app have either
been inlined into an equals override or removed outright.

The new UID format is as follows:
datatype/format:uuid

Datatype is a tag that is just the lowercase tag name. For example,
"song". Format is the program that created the UID. auxio will be an
md5 hash, and musicbrainz will the a musicbrainz ID extracted from a
file. UUID is the uuid itself.

This is much more reliable and extendable than the old ID format. This
will also be the last time I break compat with old ID formats. From now
on, a legacy UID field will not be included to enable backwards compat,
when the time comes for a breaking change.
This commit is contained in:
Alexander Capehart 2022-09-06 22:13:06 -06:00
parent 457013d047
commit 48ad45e4c3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
31 changed files with 443 additions and 360 deletions

View file

@ -2,6 +2,9 @@
## dev
#### What's New
- Reworked music hashing to be even more reliable (Will wipe playback state)
#### What's Fixed
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
- Fixed issue where the playback progress would continue in the notification even if

View file

@ -3,6 +3,7 @@ plugins {
id "kotlin-android"
id "androidx.navigation.safeargs.kotlin"
id "com.diffplug.spotless"
id "kotlin-parcelize"
}
android {

View file

@ -225,7 +225,7 @@ class MainFragment :
findNavController().navigate(MainFragmentDirections.actionShowAbout())
is MainNavigationAction.SongDetails ->
findNavController()
.navigate(MainFragmentDirections.actionShowDetails(action.song.id))
.navigate(MainFragmentDirections.actionShowDetails(action.song.uid))
}
navModel.finishMainNavigation()

View file

@ -74,7 +74,7 @@ class AlbumDetailFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setAlbumId(args.albumId)
detailModel.setAlbumUid(args.albumUid)
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_album_detail)
@ -169,7 +169,7 @@ class AlbumDetailFragment :
findNavController()
.navigate(
AlbumDetailFragmentDirections.actionShowArtist(
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid))
}
private fun handleItemChange(album: Album?) {
@ -187,28 +187,28 @@ class AlbumDetailFragment :
// Songs should be scrolled to if the album matches, or a new detail
// fragment should be launched otherwise.
is Song -> {
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) {
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
logD("Navigating to a song in this album")
scrollToItem(item.id)
scrollToItem(item)
navModel.finishExploreNavigation()
} else {
logD("Navigating to another album")
findNavController()
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid))
}
}
// If the album matches, no need to do anything. Otherwise launch a new
// detail fragment.
is Album -> {
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) {
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) {
logD("Navigating to the top of this album")
binding.detailRecycler.scrollToPosition(0)
navModel.finishExploreNavigation()
} else {
logD("Navigating to another album")
findNavController()
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.uid))
}
}
@ -216,17 +216,17 @@ class AlbumDetailFragment :
is Artist -> {
logD("Navigating to another artist")
findNavController()
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.uid))
}
null -> {}
else -> error("Unexpected navigation item ${item::class.java}")
}
}
/** Scroll to an song using its [id]. */
private fun scrollToItem(id: Long) {
/** Scroll to an [song]. */
private fun scrollToItem(song: Song) {
// Calculate where the item for the currently played song is
val pos = detailModel.albumData.value.indexOfFirst { it.id == id && it is Song }
val pos = detailModel.albumData.value.indexOf(song)
if (pos != -1) {
val binding = requireBinding()
@ -255,7 +255,7 @@ class AlbumDetailFragment :
}
}
if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
detailAdapter.updateIndicator(song, isPlaying)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
@ -279,8 +279,6 @@ class AlbumDetailFragment :
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
}

View file

@ -69,7 +69,7 @@ class ArtistDetailFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setArtistId(args.artistId)
detailModel.setArtistUid(args.artistUid)
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
@ -177,22 +177,22 @@ class ArtistDetailFragment :
is Song -> {
logD("Navigating to another album")
findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid))
}
is Album -> {
logD("Navigating to another album")
findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.uid))
}
is Artist -> {
if (item.id == detailModel.currentArtist.value?.id) {
if (item.uid == detailModel.currentArtist.value?.uid) {
logD("Navigating to the top of this artist")
binding.detailRecycler.scrollToPosition(0)
navModel.finishExploreNavigation()
} else {
logD("Navigating to another artist")
findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.uid))
}
}
null -> {}
@ -207,7 +207,7 @@ class ArtistDetailFragment :
item = parent
}
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
item = song
}

View file

@ -109,10 +109,10 @@ class DetailViewModel(application: Application) :
private val songGuard = TaskGuard()
fun setSongId(id: Long) {
if (_currentSong.value?.run { song.id } == id) return
fun setSongUid(uid: Music.UID) {
if (_currentSong.value?.run { song.uid } == uid) return
val library = unlikelyToBeNull(musicStore.library)
val song = requireNotNull(library.findSongById(id)) { "Invalid song id provided" }
val song = requireNotNull(library.find<Song>(uid)) { "Invalid song id provided" }
generateDetailSong(song)
}
@ -121,27 +121,28 @@ class DetailViewModel(application: Application) :
_currentSong.value = null
}
fun setAlbumId(id: Long) {
if (_currentAlbum.value?.id == id) return
fun setAlbumUid(uid: Music.UID) {
if (_currentAlbum.value?.uid == uid) return
val library = unlikelyToBeNull(musicStore.library)
val album = requireNotNull(library.findAlbumById(id)) { "Invalid album id provided " }
val album = requireNotNull(library.find<Album>(uid)) { "Invalid album id provided " }
_currentAlbum.value = album
refreshAlbumData(album)
}
fun setArtistId(id: Long) {
if (_currentArtist.value?.id == id) return
fun setArtistUid(uid: Music.UID) {
logD(uid)
if (_currentArtist.value?.uid == uid) return
val library = unlikelyToBeNull(musicStore.library)
val artist = requireNotNull(library.findArtistById(id)) { "Invalid artist id provided" }
val artist = requireNotNull(library.find<Artist>(uid)) { "Invalid artist id provided" }
_currentArtist.value = artist
refreshArtistData(artist)
}
fun setGenreId(id: Long) {
if (_currentGenre.value?.id == id) return
fun setGenreUid(uid: Music.UID) {
if (_currentGenre.value?.uid == uid) return
val library = unlikelyToBeNull(musicStore.library)
val genre = requireNotNull(library.findGenreById(id)) { "Invalid genre id provided" }
val genre = requireNotNull(library.find<Genre>(uid)) { "Invalid genre id provided" }
_currentGenre.value = genre
refreshGenreData(genre)
}
@ -318,12 +319,6 @@ class DetailViewModel(application: Application) :
}
}
data class SortHeader(@StringRes val string: Int) : Item() {
override val id: Long
get() = string.toLong()
}
data class SortHeader(@StringRes val string: Int) : Item
data class DiscHeader(val disc: Int) : Item() {
override val id: Long
get() = disc.toLong()
}
data class DiscHeader(val disc: Int) : Item

View file

@ -70,7 +70,7 @@ class GenreDetailFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setGenreId(args.genreId)
detailModel.setGenreUid(args.genreUid)
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
@ -125,6 +125,7 @@ class GenreDetailFragment :
override fun onOpenMenu(item: Item, anchor: View) {
if (item is Song) {
musicMenu(anchor, R.menu.menu_song_actions, item)
return
}
error("Unexpected datatype when opening menu: ${item::class.java}")
@ -170,16 +171,17 @@ class GenreDetailFragment :
is Song -> {
logD("Navigating to another song")
findNavController()
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid))
}
is Album -> {
logD("Navigating to another album")
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
findNavController()
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.uid))
}
is Artist -> {
logD("Navigating to another artist")
findNavController()
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid))
}
is Genre -> {
navModel.finishExploreNavigation()
@ -189,7 +191,7 @@ class GenreDetailFragment :
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
if (parent is Genre && parent == unlikelyToBeNull(detailModel.currentGenre.value)) {
detailAdapter.updateIndicator(song, isPlaying)
} else {
// Ignore song playback not from the genre

View file

@ -50,7 +50,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
detailModel.setSongId(args.songId)
detailModel.setSongUid(args.songUid)
collectImmediately(detailModel.currentSong, ::updateSong)
}

View file

@ -85,15 +85,15 @@ class AlbumDetailAdapter(private val listener: Listener) :
companion object {
private val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Album && newItem is Album ->
AlbumDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
AlbumDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is DiscHeader && newItem is DiscHeader ->
DiscHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
DiscHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Song && newItem is Song ->
AlbumSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
AlbumSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
}
}
}
@ -142,7 +142,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
val DIFFER =
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName &&
oldItem.artist.rawName == newItem.artist.rawName &&
oldItem.date == newItem.date &&
@ -168,7 +168,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
val DIFFER =
object : SimpleItemCallback<DiscHeader>() {
override fun areItemsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
oldItem.disc == newItem.disc
}
}
@ -216,7 +216,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
val DIFFER =
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
}
}

View file

@ -87,15 +87,15 @@ class ArtistDetailAdapter(private val listener: Listener) :
companion object {
private val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Artist && newItem is Artist ->
ArtistDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
ArtistDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Album && newItem is Album ->
ArtistAlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
ArtistAlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Song && newItem is Song ->
ArtistSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
ArtistSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
}
}
}
@ -112,7 +112,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
// Get the genre that corresponds to the most songs in this artist, which would be
// the most "Prominent" genre.
var genresByAmount = mutableMapOf<Genre, Int>()
val genresByAmount = mutableMapOf<Genre, Int>()
for (song in item.songs) {
for (genre in song.genres) {
genresByAmount[genre] = genresByAmount[genre]?.inc() ?: 1
@ -172,7 +172,7 @@ private constructor(
val DIFFER =
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.date == newItem.date
}
}
@ -207,7 +207,7 @@ private constructor(
val DIFFER =
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName &&
oldItem.album.rawName == newItem.album.rawName
}

View file

@ -95,12 +95,12 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
companion object {
val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Header && newItem is Header ->
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is SortHeader && newItem is SortHeader ->
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
SortHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
else -> false
}
}
@ -132,7 +132,7 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
val DIFFER =
object : SimpleItemCallback<SortHeader>() {
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) =
oldItem.string == newItem.string
}
}

View file

@ -79,12 +79,12 @@ class GenreDetailAdapter(private val listener: Listener) :
companion object {
val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Genre && newItem is Genre ->
GenreDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
GenreDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Song && newItem is Song ->
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
}
}
@ -113,7 +113,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
val DIFFER =
object : SimpleItemCallback<Genre>() {
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
oldItem.rawName == newItem.rawName &&
oldItem.songs.size == newItem.songs.size &&
oldItem.durationMs == newItem.durationMs

View file

@ -372,10 +372,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun handleNavigation(item: Music?) {
val action =
when (item) {
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.id)
is Album -> HomeFragmentDirections.actionShowAlbum(item.id)
is Artist -> HomeFragmentDirections.actionShowArtist(item.id)
is Genre -> HomeFragmentDirections.actionShowGenre(item.id)
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid)
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid.also { logD(it) })
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
else -> return
}

View file

@ -81,6 +81,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
override fun onOpenMenu(item: Item, anchor: View) {
if (item is Artist) {
musicMenu(anchor, R.menu.menu_genre_artist_actions, item)
return
}
error("Unexpected datatype when opening menu: ${item::class.java}")

View file

@ -39,14 +39,13 @@ import org.oxycblt.auxio.ui.Sort
/** A basic keyer for music data. */
class MusicKeyer : Keyer<Music> {
override fun key(data: Music, options: Options): String {
return if (data is Song) {
override fun key(data: Music, options: Options) =
if (data is Song) {
// Group up song covers with album covers for better caching
key(data.album, options)
data.album.uid.toString()
} else {
"${data::class.simpleName}: ${data.id}"
data.uid.toString()
}
}
}
/**
@ -65,9 +64,8 @@ private constructor(private val context: Context, private val album: Album) : Ba
}
class SongFactory : Fetcher.Factory<Song> {
override fun create(data: Song, options: Options, imageLoader: ImageLoader): Fetcher {
return AlbumCoverFetcher(options.context, data.album)
}
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data.album)
}
class AlbumFactory : Fetcher.Factory<Album> {
@ -114,7 +112,7 @@ private constructor(
// whenever possible. So, if there are more than four distinct artists in a genre, make
// it so that one artist only adds one album cover to the mosaic. Otherwise, use order
// albums normally.
val artists = genre.songs.groupBy { it.album.artist.id }.keys
val artists = genre.songs.groupBy { it.album.artist }.keys
val albums =
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
if (artists.size > 4) {

View file

@ -20,20 +20,30 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.os.Parcelable
import java.security.MessageDigest
import java.util.UUID
import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.msToSecs
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS ---
/** [Item] variant that represents a music item. */
sealed class Music : Item() {
sealed class Music : Item {
abstract val uid: UID
/** The raw name of this item. Null if unknown. */
abstract val rawName: String?
@ -52,6 +62,106 @@ sealed class Music : Item() {
* become Unknown Artist, (124) would become its proper genre name, etc.
*/
abstract fun resolveName(context: Context): String
// Equality is based on UIDs, as some items (Especially artists) can have identical
// properties (Name) yet non-identical UIDs due to MusicBrainz tags
override fun hashCode() = uid.hashCode()
override fun equals(other: Any?) =
other is Music && javaClass == other.javaClass && uid == other.uid
/** A unique identifier for a piece of music. */
@Parcelize
class UID
private constructor(val datatype: String, val isMusicBrainz: Boolean, val uuid: UUID) :
Parcelable {
@IgnoredOnParcel private val hashCode: Int
init {
var result = datatype.hashCode()
result = 31 * result + isMusicBrainz.hashCode()
result = 31 * result + uuid.hashCode()
hashCode = result
}
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is UID &&
datatype == other.datatype &&
isMusicBrainz == other.isMusicBrainz &&
uuid == other.uuid
override fun toString() = "$datatype/${if (isMusicBrainz) "musicbrainz" else "auxio"}:$uuid"
companion object {
fun fromString(uid: String): UID? {
val split = uid.split(':', limit = 2)
if (split.size != 2) {
logE("Invalid uid: Malformed structure")
}
val namespace = split[0].split('/', limit = 2)
if (namespace.size != 2) {
logE("Invalid uid: Malformed namespace")
return null
}
val datatype = namespace[0]
val isMusicBrainz =
when (namespace[1]) {
"auxio" -> false
"musicbrainz" -> true
else -> {
logE("Invalid mid: Malformed uuid type")
return null
}
}
val uuid =
try {
UUID.fromString(split[1])
} catch (e: Exception) {
logE("Invalid uid: Malformed UUID")
return null
}
return UID(datatype, isMusicBrainz, uuid)
}
fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID {
val digest = MessageDigest.getInstance("MD5")
updates(digest)
val hash = digest.digest()
val uuid =
UUID(
hash[0]
.toLong()
.shl(56)
.or(hash[1].toLong().and(0xFF).shl(48))
.or(hash[2].toLong().and(0xFF).shl(40))
.or(hash[3].toLong().and(0xFF).shl(32))
.or(hash[4].toLong().and(0xFF).shl(24))
.or(hash[5].toLong().and(0xFF).shl(16))
.or(hash[6].toLong().and(0xFF).shl(8))
.or(hash[7].toLong().and(0xFF)),
hash[8]
.toLong()
.shl(56)
.or(hash[9].toLong().and(0xFF).shl(48))
.or(hash[10].toLong().and(0xFF).shl(40))
.or(hash[11].toLong().and(0xFF).shl(32))
.or(hash[12].toLong().and(0xFF).shl(24))
.or(hash[13].toLong().and(0xFF).shl(16))
.or(hash[14].toLong().and(0xFF).shl(8))
.or(hash[15].toLong().and(0xFF)))
return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid)
}
}
}
}
/**
@ -67,17 +177,8 @@ sealed class MusicParent : Music() {
* A song.
* @author OxygenCobalt
*/
data class Song(private val raw: Raw) : Music() {
override val id: Long
get() {
var result = rawName.toMusicId()
result = 31 * result + album.rawName.toMusicId()
result = 31 * result + album.artist.rawName.toMusicId()
result = 31 * result + (track ?: 0)
result = 31 * result + (disc ?: 0)
result = 31 * result + durationMs
return result
}
class Song(private val raw: Raw) : Music() {
override val uid: UID
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
@ -113,20 +214,18 @@ data class Song(private val raw: Raw) : Music() {
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
/** The track number of this song in it's album.. */
val track = raw.track
val track: Int? = raw.track
/** The disc number of this song in it's album. */
val disc = raw.disc
val disc: Int? = raw.disc
private var _album: Album? = null
/** The album of this song. */
val album: Album
get() = unlikelyToBeNull(_album)
private var _genres: MutableList<Genre> = mutableListOf()
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
val genres: List<Genre>
get() = _genres
// TODO: Multi-artist support
// private val _artists: MutableList<Artist> = mutableListOf()
/**
* The raw artist name for this song in particular. First uses the artist tag, and then falls
@ -142,31 +241,29 @@ data class Song(private val raw: Raw) : Music() {
fun resolveIndividualArtistName(context: Context) =
raw.artistName ?: album.artist.resolveName(context)
private val _genres: MutableList<Genre> = mutableListOf()
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
val genres: List<Genre>
get() = _genres
// --- INTERNAL FIELDS ---
val _distinct =
rawName to
raw.albumName to
raw.artistName to
raw.albumArtistName to
raw.genreNames to
track to
disc to
durationMs
val _rawAlbum: Album.Raw
val _rawAlbum =
Album.Raw(
mediaStoreId = requireNotNull(raw.albumMediaStoreId) { "Invalid raw: No album id" },
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName,
date = raw.date,
releaseType = raw.albumReleaseType,
rawArtist =
if (raw.albumArtistName != null) {
Artist.Raw(raw.albumArtistName, raw.albumArtistSortName)
} else {
Artist.Raw(raw.artistName, raw.artistSortName)
})
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
val _isMissingAlbum: Boolean
get() = _album == null
val _isMissingArtist: Boolean
get() = _album?._isMissingArtist ?: true
val _isMissingGenre: Boolean
get() = _genres.isEmpty()
fun _link(album: Album) {
_album = album
}
@ -175,27 +272,28 @@ data class Song(private val raw: Raw) : Music() {
_genres.add(genre)
}
fun _validate() {
(checkNotNull(_album) { "Malformed song: album is null" })._validate()
check(_genres.isNotEmpty()) { "Malformed song: genres are empty" }
}
init {
val artistName: String?
val artistSortName: String?
// Generally, we calculate UIDs at the end since everything will definitely be initialized
// by now.
uid =
UID.hashed(this::class) {
update(rawName.lowercase())
update(_rawAlbum.name.lowercase())
update(_rawAlbum.date)
if (raw.albumArtistName != null) {
artistName = raw.albumArtistName
artistSortName = raw.albumArtistSortName
} else {
artistName = raw.artistName
artistSortName = raw.artistSortName
}
update(raw.artistName)
update(raw.albumArtistName)
_rawAlbum =
Album.Raw(
mediaStoreId = raw.albumMediaStoreId,
name = raw.albumName,
sortName = raw.albumSortName,
date = raw.date,
releaseType = raw.albumReleaseType,
artistName,
artistSortName)
update(track)
update(disc)
update(durationMs.msToSecs())
}
}
data class Raw(
@ -225,43 +323,26 @@ data class Song(private val raw: Raw) : Music() {
}
/** The data object for an album. */
data class Album(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
init {
for (song in songs) {
song._link(this)
}
}
class Album(raw: Raw, override val songs: List<Song>) : MusicParent() {
override val uid: UID
override val id: Long
get() {
var result = rawName.toMusicId()
result = 31 * result + artist.rawName.toMusicId()
result = 31 * result + (date?.year ?: 0)
return result
}
override val rawName = requireNotNull(raw.name) { "Invalid raw: No name" }
override val rawName = raw.name
override val rawSortName = raw.sortName
override fun resolveName(context: Context) = rawName
/**
* The album cover URI for this album. Usually low quality, so using Coil is recommended
* instead.
*/
val coverUri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.albumCoverUri
/** The latest date this album was released. */
val date = raw.date
/** The release type of this album, such as "EP". Defaults to "Album". */
val releaseType = raw.releaseType ?: ReleaseType.Album(null)
private var _artist: Artist? = null
/** The parent artist of this album. */
val artist: Artist
get() = unlikelyToBeNull(_artist)
/**
* The album cover URI for this album. Usually low quality, so using Coil is recommended
* instead.
*/
val coverUri = raw.mediaStoreId.albumCoverUri
/** The earliest date a song in this album was added. */
val dateAdded = songs.minOf { it.dateAdded }
@ -269,34 +350,50 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
/** The total duration of songs in this album, in millis. */
val durationMs = songs.sumOf { it.durationMs }
private var _artist: Artist? = null
/** The parent artist of this album. */
val artist: Artist
get() = unlikelyToBeNull(_artist)
// --- INTERNAL FIELDS ---
val _rawArtist: Artist.Raw
get() = Artist.Raw(name = raw.artistName, sortName = raw.artistSortName)
val _isMissingArtist: Boolean
get() = _artist == null
val _rawArtist = raw.rawArtist
fun _link(artist: Artist) {
_artist = artist
}
data class Raw(
val mediaStoreId: Long?,
val name: String?,
fun _validate() {
checkNotNull(_artist) { "Invalid album: Artist is null " }
}
init {
uid =
UID.hashed(this::class) {
update(rawName)
update(_rawArtist.name)
update(date)
}
for (song in songs) {
song._link(this)
}
}
class Raw(
val mediaStoreId: Long,
val name: String,
val sortName: String?,
val date: Date?,
val releaseType: ReleaseType?,
val artistName: String?,
val artistSortName: String?,
val rawArtist: Artist.Raw
) {
val groupingId: Long
private val hashCode = 31 * name.lowercase().hashCode() + rawArtist.hashCode()
init {
var groupingIdResult = artistName.toMusicId()
groupingIdResult = 31 * groupingIdResult + name.toMusicId()
groupingId = groupingIdResult
}
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is Raw && name.equals(other.name, true) && rawArtist == other.rawArtist
}
}
@ -304,19 +401,12 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
* artist or artist field, not the individual performers of an artist.
*/
data class Artist(
private val raw: Raw,
class Artist(
raw: Raw,
/** The albums of this artist. */
val albums: List<Album>
) : MusicParent() {
init {
for (album in albums) {
album._link(this)
}
}
override val id: Long
get() = rawName.toMusicId()
override val uid: UID
override val rawName = raw.name
@ -329,59 +419,93 @@ data class Artist(
/** The total duration of songs in this artist, in millis. */
val durationMs = songs.sumOf { it.durationMs }
data class Raw(val name: String?, val sortName: String?) {
val groupingId = name.toMusicId()
init {
uid = UID.hashed(this::class) { update(rawName) }
for (album in albums) {
album._link(this)
}
}
class Raw(val name: String?, val sortName: String?) {
private val hashCode = name?.lowercase().hashCode()
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is Raw &&
when {
name != null && other.name != null -> name.equals(other.name, true)
name == null && other.name == null -> true
else -> false
}
}
}
/** The data object for a genre. */
data class Genre(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
init {
for (song in songs) {
song._link(this)
}
}
class Genre(raw: Raw, override val songs: List<Song>) : MusicParent() {
override val uid: UID
override val rawName: String?
get() = raw.name
override val rawName = raw.name
// Sort tags don't make sense on genres
override val rawSortName: String?
get() = rawName
override val id: Long
get() = rawName.toMusicId()
override val rawSortName = rawName
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
/** The total duration of the songs in this genre, in millis. */
val durationMs = songs.sumOf { it.durationMs }
data class Raw(val name: String?) {
override fun equals(other: Any?): Boolean {
if (other !is Raw) return false
return when {
name != null && other.name != null -> name.equals(other.name, true)
name == null && other.name == null -> true
else -> false
}
}
init {
uid = UID.hashed(this::class) { update(rawName) }
override fun hashCode() = name?.lowercase().hashCode()
for (song in songs) {
song._link(this)
}
}
class Raw(val name: String?) {
private val hashCode = name?.lowercase().hashCode()
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is Raw &&
when {
name != null && other.name != null -> name.equals(other.name, true)
name == null && other.name == null -> true
else -> false
}
}
}
private fun String?.toMusicId(): Long {
if (this == null) {
// Pre-calculated hash of MediaStore.UNKNOWN_STRING
return 54493231833456
}
fun MessageDigest.update(string: String?) {
if (string == null) return
update(string.lowercase().toByteArray())
}
var result = 0L
for (ch in lowercase()) {
result = 31 * result + ch.lowercaseChar().code
}
return result
fun MessageDigest.update(date: Date?) {
if (date == null) return
update(date.toString().toByteArray())
}
fun MessageDigest.update(n: Int?) {
if (n == null) return
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
}
fun MessageDigest.update(n: Long?) {
if (n == null) return
update(
byteArrayOf(
n.toByte(),
n.shr(8).toByte(),
n.shr(16).toByte(),
n.shr(24).toByte(),
n.shr(32).toByte(),
n.shr(40).toByte(),
n.shr(56).toByte(),
n.shr(64).toByte()))
}
/**

View file

@ -75,34 +75,38 @@ class MusicStore private constructor() {
val albums: List<Album>,
val songs: List<Song>
) {
private val genreIdMap = HashMap<Long, Genre>().apply { genres.forEach { put(it.id, it) } }
private val artistIdMap =
HashMap<Long, Artist>().apply { artists.forEach { put(it.id, it) } }
private val albumIdMap = HashMap<Long, Album>().apply { albums.forEach { put(it.id, it) } }
private val songIdMap = HashMap<Long, Song>().apply { songs.forEach { put(it.id, it) } }
private val uidMap = HashMap<Music.UID, Music>()
/** Find a [Song] by it's ID. Null if no song exists with that ID. */
fun findSongById(songId: Long) = songIdMap[songId]
init {
for (song in songs) {
uidMap[song.uid] = song
}
/** Find a [Album] by it's ID. Null if no album exists with that ID. */
fun findAlbumById(albumId: Long) = albumIdMap[albumId]
for (album in albums) {
uidMap[album.uid] = album
}
/** Find a [Artist] by it's ID. Null if no artist exists with that ID. */
fun findArtistById(artistId: Long) = artistIdMap[artistId]
for (artist in artists) {
uidMap[artist.uid] = artist
}
/** Find a [Genre] by it's ID. Null if no genre exists with that ID. */
fun findGenreById(genreId: Long) = genreIdMap[genreId]
for (genre in genres) {
uidMap[genre.uid] = genre
}
}
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(song: Song) = findSongById(song.id)
fun sanitize(song: Song) = find<Song>(song.uid)
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(album: Album) = findAlbumById(album.id)
fun sanitize(album: Album) = find<Album>(album.uid)
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(artist: Artist) = findArtistById(artist.id)
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(genre: Genre) = findGenreById(genre.id)
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
/** Find a song for a [uri]. */
fun findSongForUri(context: Context, uri: Uri) =

View file

@ -229,13 +229,7 @@ class Indexer {
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
for (song in songs) {
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
error(
"Found unlinked song: ${song.rawName} [" +
"missing album: ${song._isMissingAlbum} " +
"missing artist: ${song._isMissingArtist} " +
"missing genre: ${song._isMissingGenre}]")
}
song._validate()
}
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
@ -262,7 +256,7 @@ class Indexer {
}
// Deduplicate songs to prevent (most) deformed music clones
songs = songs.distinctBy { it._distinct }.toMutableList()
songs = songs.distinctBy { it.uid }.toMutableList()
// Ensure that sorting order is consistent so that grouping is also consistent.
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
@ -287,7 +281,7 @@ class Indexer {
*/
private fun buildAlbums(songs: List<Song>): List<Album> {
val albums = mutableListOf<Album>()
val songsByAlbum = songs.groupBy { it._rawAlbum.groupingId }
val songsByAlbum = songs.groupBy { it._rawAlbum }
for (entry in songsByAlbum) {
val albumSongs = entry.value
@ -296,8 +290,7 @@ class Indexer {
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
// weird years like "0" wont show up if there are alternatives.
val templateSong =
albumSongs.maxWith(
compareBy(Sort.Mode.NullableComparator.DATE) { it._rawAlbum.date })
albumSongs.maxWith(compareBy(Sort.Mode.NullableComparator.DATE) { entry.key.date })
albums.add(Album(templateSong._rawAlbum, albumSongs))
}
@ -313,12 +306,11 @@ class Indexer {
*/
private fun buildArtists(albums: List<Album>): List<Artist> {
val artists = mutableListOf<Artist>()
val albumsByArtist = albums.groupBy { it._rawArtist.groupingId }
val albumsByArtist = albums.groupBy { it._rawArtist }
for (entry in albumsByArtist) {
// The first album will suffice for template metadata.
val templateAlbum = entry.value[0]
artists.add(Artist(templateAlbum._rawArtist, albums = entry.value))
artists.add(Artist(entry.key, entry.value))
}
logD("Successfully built ${artists.size} artists")
@ -340,7 +332,7 @@ class Indexer {
}
for (entry in songsByGenre) {
genres.add(Genre(entry.key, songs = entry.value))
genres.add(Genre(entry.key, entry.value))
}
logD("Successfully built ${genres.size} genres")

View file

@ -84,7 +84,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// User wants album gain to be used when in an album, track gain otherwise.
ReplayGainMode.DYNAMIC ->
playbackManager.parent is Album &&
playbackManager.song?.album?.id == playbackManager.parent?.id
playbackManager.song?.album == playbackManager.parent
}
val resolvedGain =

View file

@ -21,11 +21,8 @@ import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getLongOrNull
import androidx.core.database.sqlite.transaction
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
@ -78,11 +75,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
private fun constructStateTable(command: StringBuilder): StringBuilder {
command
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
.append("${StateColumns.COLUMN_SONG_ID} LONG,")
.append("${StateColumns.COLUMN_SONG_UID} STRING,")
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
.append("${StateColumns.COLUMN_PARENT_ID} LONG,")
.append("${StateColumns.COLUMN_PARENT_UID} STRING,")
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
@ -93,8 +89,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
private fun constructQueueTable(command: StringBuilder): StringBuilder {
command
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
.append("${QueueColumns.SONG_ID} INTEGER NOT NULL,")
.append("${QueueColumns.ALBUM_ID} INTEGER NOT NULL)")
.append("${QueueColumns.SONG_UID} STRING NOT NULL)")
return command
}
@ -109,17 +104,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
// Correct the index to match up with a possibly shortened queue (file removals/changes)
var actualIndex = rawState.index
while (queue.getOrNull(actualIndex)?.id != rawState.songId && actualIndex > -1) {
while (queue.getOrNull(actualIndex)?.uid?.also { logD(it) } != rawState.songUid &&
actualIndex > -1) {
actualIndex--
}
val parent =
when (rawState.playbackMode) {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ALBUM -> rawState.parentId?.let(library::findAlbumById)
PlaybackMode.IN_ARTIST -> rawState.parentId?.let(library::findArtistById)
PlaybackMode.IN_GENRE -> rawState.parentId?.let(library::findGenreById)
}
val parent = rawState.parentUid?.let { library.find<MusicParent>(it) }
return SavedState(
index = actualIndex,
@ -140,11 +130,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
val playbackModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
val songIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_ID)
val parentIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_ID)
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_UID)
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_UID)
cursor.moveToFirst()
@ -154,10 +143,9 @@ class PlaybackStateDatabase private constructor(context: Context) :
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
?: RepeatMode.NONE,
isShuffled = cursor.getInt(shuffleIndex) == 1,
songId = cursor.getLong(songIdIndex),
parentId = cursor.getLongOrNull(parentIdIndex),
playbackMode = PlaybackMode.fromInt(cursor.getInt(playbackModeIndex))
?: PlaybackMode.ALL_SONGS)
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
?: return@queryAll null,
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
}
}
@ -168,9 +156,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_ID)
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
while (cursor.moveToNext()) {
library.findSongById(cursor.getLong(songIndex))?.let(queue::add)
logD(cursor.getString(songIndex))
val uid = Music.UID.fromString(cursor.getString(songIndex)) ?: continue
val song = library.find<Song>(uid) ?: continue
queue.add(song)
}
}
@ -190,15 +181,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
positionMs = state.positionMs,
repeatMode = state.repeatMode,
isShuffled = state.isShuffled,
songId = state.queue[state.index].id,
parentId = state.parent?.id,
playbackMode =
when (state.parent) {
null -> PlaybackMode.ALL_SONGS
is Album -> PlaybackMode.IN_ALBUM
is Artist -> PlaybackMode.IN_ARTIST
is Genre -> PlaybackMode.IN_GENRE
})
songUid = state.queue[state.index].uid,
parentUid = state.parent?.uid)
writeRawState(rawState)
writeQueue(state.queue)
@ -218,11 +202,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
val stateData =
ContentValues(10).apply {
put(StateColumns.COLUMN_ID, 0)
put(StateColumns.COLUMN_SONG_ID, rawState.songId)
put(StateColumns.COLUMN_SONG_UID, rawState.songUid.toString())
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
put(StateColumns.COLUMN_PARENT_ID, rawState.parentId)
put(StateColumns.COLUMN_PARENT_UID, rawState.parentUid?.toString())
put(StateColumns.COLUMN_INDEX, rawState.index)
put(StateColumns.COLUMN_PLAYBACK_MODE, rawState.playbackMode.intCode)
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
}
@ -255,8 +238,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
val itemData =
ContentValues(4).apply {
put(QueueColumns.ID, idStart + i)
put(QueueColumns.SONG_ID, song.id)
put(QueueColumns.ALBUM_ID, song.album.id)
put(QueueColumns.SONG_UID, song.uid.toString())
}
insert(TABLE_NAME_QUEUE, null, itemData)
@ -286,31 +268,28 @@ class PlaybackStateDatabase private constructor(context: Context) :
val positionMs: Long,
val repeatMode: RepeatMode,
val isShuffled: Boolean,
val songId: Long,
val parentId: Long?,
val playbackMode: PlaybackMode
val songUid: Music.UID,
val parentUid: Music.UID?
)
private object StateColumns {
const val COLUMN_ID = "id"
const val COLUMN_SONG_ID = "song"
const val COLUMN_SONG_UID = "song_uid"
const val COLUMN_POSITION = "position"
const val COLUMN_PARENT_ID = "parent"
const val COLUMN_PARENT_UID = "parent"
const val COLUMN_INDEX = "queue_index"
const val COLUMN_PLAYBACK_MODE = "playback_mode"
const val COLUMN_IS_SHUFFLED = "is_shuffling"
const val COLUMN_REPEAT_MODE = "repeat_mode"
}
private object QueueColumns {
const val ID = "id"
const val SONG_ID = "song"
const val ALBUM_ID = "album"
const val SONG_UID = "song_uid"
}
companion object {
const val DB_NAME = "auxio_state_database.db"
const val DB_VERSION = 7
const val DB_VERSION = 8
const val TABLE_NAME_STATE = "playback_state_table"
const val TABLE_NAME_QUEUE = "queue_table"

View file

@ -449,7 +449,7 @@ class PlaybackStateManager private constructor() {
// While we could just save and reload the state, we instead sanitize the state
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
val oldSongId = song?.id
val oldSongUid = song?.uid
val oldPosition = playerState.calculateElapsedPosition()
parent =
@ -463,7 +463,7 @@ class PlaybackStateManager private constructor() {
_queue = newLibrary.sanitize(_queue).toMutableList()
while (song?.id != oldSongId && index > -1) {
while (song?.uid != oldSongUid && index > -1) {
index--
}

View file

@ -196,7 +196,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
// instead of loading a bitmap.
val description =
MediaDescriptionCompat.Builder()
.setMediaId("Song:${song.id}")
.setMediaId(song.uid.toString())
.setTitle(song.resolveName(context))
.setSubtitle(song.resolveIndividualArtistName(context))
.setIconUri(song.album.coverUri)

View file

@ -356,7 +356,7 @@ class PlaybackService :
}
is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(song, settings.libPlaybackMode, settings)
playbackManager.play(song, null, settings)
}
}
}

View file

@ -90,18 +90,18 @@ class SearchAdapter(private val listener: MenuItemListener) :
companion object {
private val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
when {
oldItem is Song && newItem is Song ->
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Album && newItem is Album ->
AlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
AlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Artist && newItem is Artist ->
ArtistViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
ArtistViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Genre && newItem is Genre ->
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
GenreViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
oldItem is Header && newItem is Header ->
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
else -> false
}
}

View file

@ -170,10 +170,10 @@ class SearchFragment :
findNavController()
.navigate(
when (item) {
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
else -> return
})

View file

@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* similarly distorted by the insets, and thus I must go further and modify the edge effect to be at
* least somewhat clamped to the insets themselves.
* 3. Touch events. Bottom sheets must always intercept touches in their bounds, or they will click
* the now overlapping content view that is only inset by it and not unhidden by it.
* the now overlapping content view that is only inset and not moved out of the way..
*
* @author OxygenCobalt
*/

View file

@ -40,7 +40,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
holder.updateIndicator(
currentItem != null &&
item.javaClass == currentItem.javaClass &&
item.id == currentItem.id,
item == currentItem,
isPlaying)
}
}
@ -57,7 +57,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
if (oldItem != null) {
val pos =
currentList.indexOfFirst {
it.javaClass == oldItem.javaClass && it.id == oldItem.id
it.javaClass == oldItem.javaClass && item == currentItem
}
if (pos > -1) {
@ -68,8 +68,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
}
if (item != null) {
val pos =
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
@ -85,8 +84,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
this.isPlaying = isPlaying
if (!updatedItem && item != null) {
val pos =
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)

View file

@ -23,23 +23,14 @@ import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/**
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
* provided for free by the normal adapter implementations, such as certain types of diffing.
*/
abstract class Item {
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
abstract val id: Long
}
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
interface Item
/** A data object used solely for the "Header" UI element. */
data class Header(
/** The string resource used for the header. */
@StringRes val string: Int
) : Item() {
override val id: Long
get() = string.toLong()
}
) : Item
/** An interface for detecting if an item has been clicked once. */
interface ItemClickListener {
@ -165,8 +156,5 @@ class SyncListDiffer<T>(
* [areContentsTheSame] any object that is derived from [Item].
*/
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
if (oldItem.javaClass != newItem.javaClass) return false
return oldItem.id == newItem.id
}
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
}

View file

@ -62,7 +62,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
val DIFFER =
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName &&
oldItem.individualArtistRawName == oldItem.individualArtistRawName
}
@ -102,7 +102,7 @@ private constructor(
val DIFFER =
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName &&
oldItem.artist.rawName == newItem.artist.rawName &&
oldItem.releaseType == newItem.releaseType
@ -145,7 +145,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
val DIFFER =
object : SimpleItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName &&
oldItem.albums.size == newItem.albums.size &&
oldItem.songs.size == newItem.songs.size
@ -187,7 +187,7 @@ private constructor(
val DIFFER =
object : SimpleItemCallback<Genre>() {
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
}
}
@ -211,7 +211,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
val DIFFER =
object : SimpleItemCallback<Header>() {
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
oldItem.string == newItem.string
}
}

View file

@ -8,7 +8,7 @@
Now, it would be quite cool if we could implement shared element transitions
between elements in this navigation web. Sadly though, the shared element transition
system is filled with so many bugs and visual errors to make this a terrible idea.
Just use the boring, yet sane and functional fade transitions.
Just use the boring, yet sane and functional axis transitions.
-->
<fragment
@ -17,8 +17,8 @@
android:label="ArtistDetailFragment"
tools:layout="@layout/fragment_detail">
<argument
android:name="artistId"
app:argType="long" />
android:name="artistUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
<action
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment" />
@ -32,8 +32,8 @@
android:label="AlbumDetailFragment"
tools:layout="@layout/fragment_detail">
<argument
android:name="albumId"
app:argType="long" />
android:name="albumUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
<action
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment" />
@ -47,8 +47,8 @@
android:label="GenreDetailFragment"
tools:layout="@layout/fragment_detail">
<argument
android:name="genreId"
app:argType="long" />
android:name="genreUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
<action
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment" />

View file

@ -40,7 +40,7 @@
android:label="song_detail_dialog"
tools:layout="@layout/dialog_song_detail">
<argument
android:name="songId"
app:argType="long" />
android:name="songUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
</navigation>