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:
parent
d22de34fd3
commit
31d647123f
11 changed files with 100 additions and 48 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue