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.
logD("Implicit albums present, adding to list")
@Suppress("UNCHECKED_CAST")
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] =
(grouping as MutableMap<AlbumGrouping, Collection<Album>>)[AlbumGrouping.APPEARANCES] =
artist.implicitAlbums
}
@ -490,7 +490,7 @@ constructor(
val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(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.
@ -519,7 +519,7 @@ constructor(
val artistHeader = BasicHeader(R.string.lbl_artists)
list.add(Divider(artistHeader))
list.add(artistHeader)
list.addAll(genre.artists)
list.addAll(GENRE_ARTIST_SORT.artists(genre.artists))
val songHeader = SortHeader(R.string.lbl_songs)
list.add(Divider(songHeader))
@ -576,4 +576,9 @@ constructor(
LIVE(R.string.lbl_live_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 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 =
ImageRequest.Builder(context)
.data(songs)

View file

@ -27,22 +27,22 @@ import javax.inject.Inject
import org.oxycblt.auxio.music.Song
class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) :
Keyer<List<Song>> {
override fun key(data: List<Song>, options: Options) =
Keyer<Collection<Song>> {
override fun key(data: Collection<Song>, options: Options) =
"${coverExtractor.computeCoverOrdering(data).hashCode()}"
}
class SongCoverFetcher
private constructor(
private val songs: List<Song>,
private val songs: Collection<Song>,
private val size: Size,
private val coverExtractor: CoverExtractor,
) : Fetcher {
override suspend fun fetch() = coverExtractor.extract(songs, size)
class Factory @Inject constructor(private val coverExtractor: CoverExtractor) :
Fetcher.Factory<List<Song>> {
override fun create(data: List<Song>, options: Options, imageLoader: ImageLoader) =
Fetcher.Factory<Collection<Song>> {
override fun create(data: Collection<Song>, options: Options, imageLoader: ImageLoader) =
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
* [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 streams = mutableListOf<InputStream>()
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
* 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
if (songs.isEmpty()) return listOf()
if (songs.size == 1) return listOf(songs.first().album)
@ -150,7 +150,7 @@ constructor(
MediaMetadataRetriever().run {
// 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
setDataSource(context, album.songs[0].uri)
setDataSource(context, album.coverUri.song)
// Get the embedded picture from MediaMetadataRetriever, which will return a full
// ByteArray of the cover without any compression artifacts.
@ -161,7 +161,7 @@ constructor(
private suspend fun extractExoplayerCover(album: Album): InputStream? {
val tracks =
MetadataRetriever.retrieveMetadata(
mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri))
mediaSourceFactory, MediaItem.fromUri(album.coverUri.song))
.asDeferred()
.await()
@ -207,7 +207,9 @@ constructor(
private suspend fun extractMediaStoreCover(album: Album) =
// 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 */
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 kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.Path
@ -225,7 +226,7 @@ sealed interface Music : Item {
*/
sealed interface MusicParent : Music {
/** 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
* cost of image quality.
*/
val coverUri: Uri
val coverUri: CoverUri
/** The duration of all songs in the album, in milliseconds. */
val durationMs: Long
/** 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
* "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. */
val explicitAlbums: List<Album>
val explicitAlbums: Collection<Album>
/** 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
* songs.
@ -345,7 +343,7 @@ interface Artist : MusicParent {
*/
interface Genre : MusicParent {
/** 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. */
val durationMs: Long
}
@ -356,6 +354,7 @@ interface Genre : MusicParent {
* @author Alexander Capehart (OxygenCobalt)
*/
interface Playlist : MusicParent {
override val songs: List<Song>
/** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long
}

View file

@ -160,7 +160,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else {
// Need to initialize this grouping.
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.
@ -174,7 +174,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} else {
// Need to initialize this grouping.
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 {
// Need to initialize this grouping.
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 {
// Need to initialize this grouping.
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
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album
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 dates: Date.Range?
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 dateAdded: Long
override val songs: List<Song>
private val _artists = mutableListOf<ArtistImpl>()
override val artists: List<Artist>
get() = _artists
override val songs: Set<Song> = grouping.music
private var hashCode = uid.hashCode()
init {
@ -298,7 +300,6 @@ class AlbumImpl(
dates = if (min != null && max != null) Date.Range(min, max) else null
durationMs = totalDuration
dateAdded = earliestDateAdded
songs = Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(grouping.music)
hashCode = 31 * hashCode + rawAlbum.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) }
?: Name.Unknown(R.string.def_artist)
override val songs: List<Song>
override val albums: List<Album>
override val explicitAlbums: List<Album>
override val implicitAlbums: List<Album>
override val songs: Set<Song>
override val albums: Set<Album>
override val explicitAlbums: Set<Album>
override val implicitAlbums: Set<Album>
override val durationMs: Long?
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)
albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys)
explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) }
implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) }
songs = distinctSongs
albums = albumMap.keys
explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true }
implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true }
durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
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) }
?: Name.Unknown(R.string.def_genre)
override val songs: List<Song>
override val artists: List<Artist>
override val songs: Set<Song>
override val artists: Set<Artist>
override val durationMs: Long
private var hashCode = uid.hashCode()
@ -473,8 +474,8 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
totalDuration += song.durationMs
}
songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music)
artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists)
songs = grouping.music
artists = distinctArtists
durationMs = totalDuration
hashCode = 31 * hashCode + rawGenre.hashCode()
@ -510,7 +511,3 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
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))
// Since we usually have to load many songs into the queue, use the
// MediaStore URI instead of loading a bitmap.
.setIconUri(song.album.coverUri)
.setIconUri(song.album.coverUri.mediaStore)
.setMediaUri(song.uri)
.build()
// 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.
*/
fun Context.share(songs: List<Song>) {
fun Context.share(songs: Collection<Song>) {
if (songs.isEmpty()) return
logD("Showing sharesheet for ${songs.size} songs")
val builder = ShareCompat.IntentBuilder(this)