music: use set for child information

Use sets for all child music information.

Unlike parent information, which usually has an ordering derived from
file information, child music information more or less doesn't, and
will be consistently re-interpreted by the app to apply user-configured
sorts.
This commit is contained in:
Alexander Capehart 2023-06-13 10:40:55 -06:00
parent d22de34fd3
commit 31d647123f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 100 additions and 48 deletions

View file

@ -480,7 +480,7 @@ constructor(
// implicit album list into the mapping. // implicit album list into the mapping.
logD("Implicit albums present, adding to list") logD("Implicit albums present, adding to list")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] = (grouping as MutableMap<AlbumGrouping, Collection<Album>>)[AlbumGrouping.APPEARANCES] =
artist.implicitAlbums artist.implicitAlbums
} }
@ -490,7 +490,7 @@ constructor(
val header = BasicHeader(entry.key.headerTitleRes) val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(header)) list.add(Divider(header))
list.add(header) list.add(header)
list.addAll(entry.value) list.addAll(ARTIST_ALBUM_SORT.albums(entry.value))
} }
// Artists may not be linked to any songs, only include a header entry if we have any. // Artists may not be linked to any songs, only include a header entry if we have any.
@ -519,7 +519,7 @@ constructor(
val artistHeader = BasicHeader(R.string.lbl_artists) val artistHeader = BasicHeader(R.string.lbl_artists)
list.add(Divider(artistHeader)) list.add(Divider(artistHeader))
list.add(artistHeader) list.add(artistHeader)
list.addAll(genre.artists) list.addAll(GENRE_ARTIST_SORT.artists(genre.artists))
val songHeader = SortHeader(R.string.lbl_songs) val songHeader = SortHeader(R.string.lbl_songs)
list.add(Divider(songHeader)) list.add(Divider(songHeader))
@ -576,4 +576,9 @@ constructor(
LIVE(R.string.lbl_live_group), LIVE(R.string.lbl_live_group),
REMIXES(R.string.lbl_remix_group), REMIXES(R.string.lbl_remix_group),
} }
private companion object {
val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
}
} }

View file

@ -379,7 +379,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* @param desc The content description to describe the bound data. * @param desc The content description to describe the bound data.
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded. * @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/ */
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) { fun bind(songs: Collection<Song>, desc: String, @DrawableRes errorRes: Int) {
val request = val request =
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(songs) .data(songs)

View file

@ -27,22 +27,22 @@ import javax.inject.Inject
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) : class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) :
Keyer<List<Song>> { Keyer<Collection<Song>> {
override fun key(data: List<Song>, options: Options) = override fun key(data: Collection<Song>, options: Options) =
"${coverExtractor.computeCoverOrdering(data).hashCode()}" "${coverExtractor.computeCoverOrdering(data).hashCode()}"
} }
class SongCoverFetcher class SongCoverFetcher
private constructor( private constructor(
private val songs: List<Song>, private val songs: Collection<Song>,
private val size: Size, private val size: Size,
private val coverExtractor: CoverExtractor, private val coverExtractor: CoverExtractor,
) : Fetcher { ) : Fetcher {
override suspend fun fetch() = coverExtractor.extract(songs, size) override suspend fun fetch() = coverExtractor.extract(songs, size)
class Factory @Inject constructor(private val coverExtractor: CoverExtractor) : class Factory @Inject constructor(private val coverExtractor: CoverExtractor) :
Fetcher.Factory<List<Song>> { Fetcher.Factory<Collection<Song>> {
override fun create(data: List<Song>, options: Options, imageLoader: ImageLoader) = override fun create(data: Collection<Song>, options: Options, imageLoader: ImageLoader) =
SongCoverFetcher(data, options.size, coverExtractor) SongCoverFetcher(data, options.size, coverExtractor)
} }
} }

View file

@ -77,7 +77,7 @@ constructor(
* will be returned of a mosaic composed of four album covers ordered by * will be returned of a mosaic composed of four album covers ordered by
* [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned. * [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned.
*/ */
suspend fun extract(songs: List<Song>, size: Size): FetchResult? { suspend fun extract(songs: Collection<Song>, size: Size): FetchResult? {
val albums = computeCoverOrdering(songs) val albums = computeCoverOrdering(songs)
val streams = mutableListOf<InputStream>() val streams = mutableListOf<InputStream>()
for (album in albums) { for (album in albums) {
@ -117,7 +117,7 @@ constructor(
* by their names. "Representation" is defined by how many [Song]s were found to be linked to * by their names. "Representation" is defined by how many [Song]s were found to be linked to
* the given [Album] in the given [Song] list. * the given [Album] in the given [Song] list.
*/ */
fun computeCoverOrdering(songs: List<Song>): List<Album> { fun computeCoverOrdering(songs: Collection<Song>): List<Album> {
// TODO: Start short-circuiting in more places // TODO: Start short-circuiting in more places
if (songs.isEmpty()) return listOf() if (songs.isEmpty()) return listOf()
if (songs.size == 1) return listOf(songs.first().album) if (songs.size == 1) return listOf(songs.first().album)
@ -150,7 +150,7 @@ constructor(
MediaMetadataRetriever().run { MediaMetadataRetriever().run {
// This call is time-consuming but it also doesn't seem to hold up the main thread, // This call is time-consuming but it also doesn't seem to hold up the main thread,
// so it's probably fine not to wrap it.rmt // so it's probably fine not to wrap it.rmt
setDataSource(context, album.songs[0].uri) setDataSource(context, album.coverUri.song)
// Get the embedded picture from MediaMetadataRetriever, which will return a full // Get the embedded picture from MediaMetadataRetriever, which will return a full
// ByteArray of the cover without any compression artifacts. // ByteArray of the cover without any compression artifacts.
@ -161,7 +161,7 @@ constructor(
private suspend fun extractExoplayerCover(album: Album): InputStream? { private suspend fun extractExoplayerCover(album: Album): InputStream? {
val tracks = val tracks =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri)) mediaSourceFactory, MediaItem.fromUri(album.coverUri.song))
.asDeferred() .asDeferred()
.await() .await()
@ -207,7 +207,9 @@ constructor(
private suspend fun extractMediaStoreCover(album: Album) = private suspend fun extractMediaStoreCover(album: Album) =
// Eliminate any chance that this blocking call might mess up the loading process // Eliminate any chance that this blocking call might mess up the loading process
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(album.coverUri.mediaStore)
}
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult { private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
* CoverUri.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.extractor
import android.net.Uri
/**
* Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading
* images.
* @param mediaStore The album cover [Uri] obtained from MediaStore.
* @param song The [Uri] of the first song (by track) of the album, which can also be used to
* obtain an album cover.
* @author Alexander Capehart (OxygenCobalt)
*/
data class CoverUri(val mediaStore: Uri, val song: Uri)

View file

@ -27,6 +27,7 @@ import java.util.UUID
import kotlin.math.max import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.Path
@ -225,7 +226,7 @@ sealed interface Music : Item {
*/ */
sealed interface MusicParent : Music { sealed interface MusicParent : Music {
/** The child [Song]s of this [MusicParent]. */ /** The child [Song]s of this [MusicParent]. */
val songs: List<Song> val songs: Collection<Song>
} }
/** /**
@ -296,7 +297,7 @@ interface Album : MusicParent {
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* cost of image quality. * cost of image quality.
*/ */
val coverUri: Uri val coverUri: CoverUri
/** The duration of all songs in the album, in milliseconds. */ /** The duration of all songs in the album, in milliseconds. */
val durationMs: Long val durationMs: Long
/** The earliest date a song in this album was added, as a unix epoch timestamp. */ /** The earliest date a song in this album was added, as a unix epoch timestamp. */
@ -321,14 +322,11 @@ interface Artist : MusicParent {
* Note that any [Song] credited to this artist will have it's [Album] considered to be * Note that any [Song] credited to this artist will have it's [Album] considered to be
* "indirectly" linked to this [Artist], and thus included in this list. * "indirectly" linked to this [Artist], and thus included in this list.
*/ */
val albums: List<Album> val albums: Collection<Album>
/** Albums directly credited to this [Artist] via a "Album Artist" tag. */ /** Albums directly credited to this [Artist] via a "Album Artist" tag. */
val explicitAlbums: List<Album> val explicitAlbums: Collection<Album>
/** Albums indirectly credited to this [Artist] via an "Artist" tag. */ /** Albums indirectly credited to this [Artist] via an "Artist" tag. */
val implicitAlbums: List<Album> val implicitAlbums: Collection<Album>
/** /**
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* songs. * songs.
@ -345,7 +343,7 @@ interface Artist : MusicParent {
*/ */
interface Genre : MusicParent { interface Genre : MusicParent {
/** The artists indirectly linked to by the [Artist]s of this [Genre]. */ /** The artists indirectly linked to by the [Artist]s of this [Genre]. */
val artists: List<Artist> val artists: Collection<Artist>
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
} }
@ -356,6 +354,7 @@ interface Genre : MusicParent {
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface Playlist : MusicParent { interface Playlist : MusicParent {
override val songs: List<Song>
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
} }

View file

@ -160,7 +160,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else { } else {
// Need to initialize this grouping. // Need to initialize this grouping.
albumGrouping[albumKey] = albumGrouping[albumKey] =
Grouping(PrioritizedRaw(song.rawAlbum, song), mutableListOf(song)) Grouping(PrioritizedRaw(song.rawAlbum, song), mutableSetOf(song))
} }
// Group the song into each of it's artists. // Group the song into each of it's artists.
@ -174,7 +174,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else { } else {
// Need to initialize this grouping. // Need to initialize this grouping.
artistGrouping[artistKey] = artistGrouping[artistKey] =
Grouping(PrioritizedRaw(rawArtist, song), mutableListOf(song)) Grouping(PrioritizedRaw(rawArtist, song), mutableSetOf(song))
} }
} }
@ -194,7 +194,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else { } else {
// Need to initialize this grouping. // Need to initialize this grouping.
genreGrouping[genreKey] = genreGrouping[genreKey] =
Grouping(PrioritizedRaw(rawGenre, song), mutableListOf(song)) Grouping(PrioritizedRaw(rawGenre, song), mutableSetOf(song))
} }
} }
@ -230,7 +230,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else { } else {
// Need to initialize this grouping. // Need to initialize this grouping.
artistGrouping[key] = artistGrouping[key] =
Grouping(PrioritizedRaw(rawArtist, album), mutableListOf(album)) Grouping(PrioritizedRaw(rawArtist, album), mutableSetOf(album))
} }
} }
} }

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music.device package org.oxycblt.auxio.music.device
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -254,15 +255,16 @@ class AlbumImpl(
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
override val dates: Date.Range? override val dates: Date.Range?
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
override val coverUri = rawAlbum.mediaStoreId.toCoverUri() override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
override val durationMs: Long override val durationMs: Long
override val dateAdded: Long override val dateAdded: Long
override val songs: List<Song>
private val _artists = mutableListOf<ArtistImpl>() private val _artists = mutableListOf<ArtistImpl>()
override val artists: List<Artist> override val artists: List<Artist>
get() = _artists get() = _artists
override val songs: Set<Song> = grouping.music
private var hashCode = uid.hashCode() private var hashCode = uid.hashCode()
init { init {
@ -298,7 +300,6 @@ class AlbumImpl(
dates = if (min != null && max != null) Date.Range(min, max) else null dates = if (min != null && max != null) Date.Range(min, max) else null
durationMs = totalDuration durationMs = totalDuration
dateAdded = earliestDateAdded dateAdded = earliestDateAdded
songs = Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(grouping.music)
hashCode = 31 * hashCode + rawAlbum.hashCode() hashCode = 31 * hashCode + rawAlbum.hashCode()
hashCode = 31 * hashCode + songs.hashCode() hashCode = 31 * hashCode + songs.hashCode()
@ -363,10 +364,10 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
?: Name.Unknown(R.string.def_artist) ?: Name.Unknown(R.string.def_artist)
override val songs: List<Song> override val songs: Set<Song>
override val albums: List<Album> override val albums: Set<Album>
override val explicitAlbums: List<Album> override val explicitAlbums: Set<Album>
override val implicitAlbums: List<Album> override val implicitAlbums: Set<Album>
override val durationMs: Long? override val durationMs: Long?
override lateinit var genres: List<Genre> override lateinit var genres: List<Genre>
@ -394,10 +395,10 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
} }
} }
songs = Sort(Sort.Mode.ByDate, Sort.Direction.ASCENDING).songs(distinctSongs) songs = distinctSongs
albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) albums = albumMap.keys
explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true }
implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true }
durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
hashCode = 31 * hashCode + rawArtist.hashCode() hashCode = 31 * hashCode + rawArtist.hashCode()
@ -457,8 +458,8 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
?: Name.Unknown(R.string.def_genre) ?: Name.Unknown(R.string.def_genre)
override val songs: List<Song> override val songs: Set<Song>
override val artists: List<Artist> override val artists: Set<Artist>
override val durationMs: Long override val durationMs: Long
private var hashCode = uid.hashCode() private var hashCode = uid.hashCode()
@ -473,8 +474,8 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
totalDuration += song.durationMs totalDuration += song.durationMs
} }
songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music) songs = grouping.music
artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) artists = distinctArtists
durationMs = totalDuration durationMs = totalDuration
hashCode = 31 * hashCode + rawGenre.hashCode() hashCode = 31 * hashCode + rawGenre.hashCode()
@ -510,7 +511,3 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
return this return this
} }
} }
data class Grouping<R, M : Music>(var raw: PrioritizedRaw<R, M>, val music: MutableList<M>)
data class PrioritizedRaw<R, M : Music>(val inner: R, val src: M)

View file

@ -237,3 +237,21 @@ data class RawGenre(
} }
} }
} }
/**
* Represents grouped music information and the prioritized raw information to eventually derive a
* [Music] implementation instance from.
*
* @param raw The current [PrioritizedRaw] that will be used for the finalized music information.
* @param music The child [Music] instances of the music information to be created.
*/
data class Grouping<R, M : Music>(var raw: PrioritizedRaw<R, M>, val music: MutableSet<M>)
/**
* Represents a [RawAlbum], [RawArtist], or [RawGenre] specifically chosen to create a [Music]
* instance from due to it being the most likely source of truth.
*
* @param inner The raw music instance that will be used.
* @param src The [Music] instance that the raw information was derived from.
*/
data class PrioritizedRaw<R, M : Music>(val inner: R, val src: M)

View file

@ -367,7 +367,7 @@ constructor(
.setSubtitle(song.artists.resolveNames(context)) .setSubtitle(song.artists.resolveNames(context))
// Since we usually have to load many songs into the queue, use the // Since we usually have to load many songs into the queue, use the
// MediaStore URI instead of loading a bitmap. // MediaStore URI instead of loading a bitmap.
.setIconUri(song.album.coverUri) .setIconUri(song.album.coverUri.mediaStore)
.setMediaUri(song.uri) .setMediaUri(song.uri)
.build() .build()
// Store the item index so we can then use the analogous index in the // Store the item index so we can then use the analogous index in the

View file

@ -286,7 +286,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs)
* *
* @param songs The [Song]s to share. * @param songs The [Song]s to share.
*/ */
fun Context.share(songs: List<Song>) { fun Context.share(songs: Collection<Song>) {
if (songs.isEmpty()) return if (songs.isEmpty()) return
logD("Showing sharesheet for ${songs.size} songs") logD("Showing sharesheet for ${songs.size} songs")
val builder = ShareCompat.IntentBuilder(this) val builder = ShareCompat.IntentBuilder(this)