From 31d647123f11256e55b1efe1ad5139fce286fb7d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 10:40:55 -0600 Subject: [PATCH] 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. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 11 ++++-- .../java/org/oxycblt/auxio/image/CoverView.kt | 2 +- .../auxio/image/extractor/Components.kt | 10 +++--- .../auxio/image/extractor/CoverExtractor.kt | 12 ++++--- .../oxycblt/auxio/image/extractor/CoverUri.kt | 31 ++++++++++++++++ .../java/org/oxycblt/auxio/music/Music.kt | 17 +++++---- .../auxio/music/device/DeviceLibrary.kt | 8 ++--- .../auxio/music/device/DeviceMusicImpl.kt | 35 +++++++++---------- .../oxycblt/auxio/music/device/RawMusic.kt | 18 ++++++++++ .../playback/system/MediaSessionComponent.kt | 2 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 2 +- 11 files changed, 100 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index f18321430..c63c76151 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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.APPEARANCES] = + (grouping as MutableMap>)[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) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index a4d0e6917..b2912595c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -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, desc: String, @DrawableRes errorRes: Int) { + fun bind(songs: Collection, desc: String, @DrawableRes errorRes: Int) { val request = ImageRequest.Builder(context) .data(songs) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 017a76747..b7a7183db 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -27,22 +27,22 @@ import javax.inject.Inject import org.oxycblt.auxio.music.Song class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) : - Keyer> { - override fun key(data: List, options: Options) = + Keyer> { + override fun key(data: Collection, options: Options) = "${coverExtractor.computeCoverOrdering(data).hashCode()}" } class SongCoverFetcher private constructor( - private val songs: List, + private val songs: Collection, 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> { - override fun create(data: List, options: Options, imageLoader: ImageLoader) = + Fetcher.Factory> { + override fun create(data: Collection, options: Options, imageLoader: ImageLoader) = SongCoverFetcher(data, options.size, coverExtractor) } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 067b8361c..6ca126256 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -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, size: Size): FetchResult? { + suspend fun extract(songs: Collection, size: Size): FetchResult? { val albums = computeCoverOrdering(songs) val streams = mutableListOf() 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): List { + fun computeCoverOrdering(songs: Collection): List { // 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, size: Size): FetchResult { diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt new file mode 100644 index 000000000..4aadf932e --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt @@ -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 . + */ + +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) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 395b17d93..fc8a51390 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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 + val songs: Collection } /** @@ -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 - + val albums: Collection /** Albums directly credited to this [Artist] via a "Album Artist" tag. */ - val explicitAlbums: List - + val explicitAlbums: Collection /** Albums indirectly credited to this [Artist] via an "Artist" tag. */ - val implicitAlbums: List - + val implicitAlbums: Collection /** * 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 + val artists: Collection /** 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 /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 613a6473b..5a70dc578 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -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)) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index ce18f6880..9cb4d70b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -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 private val _artists = mutableListOf() override val artists: List get() = _artists + override val songs: Set = 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, musicSettings: MusicSetti rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } ?: Name.Unknown(R.string.def_artist) - override val songs: List - override val albums: List - override val explicitAlbums: List - override val implicitAlbums: List + override val songs: Set + override val albums: Set + override val explicitAlbums: Set + override val implicitAlbums: Set override val durationMs: Long? override lateinit var genres: List @@ -394,10 +395,10 @@ class ArtistImpl(grouping: Grouping, 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, musicSettings: MusicSett rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } ?: Name.Unknown(R.string.def_genre) - override val songs: List - override val artists: List + override val songs: Set + override val artists: Set override val durationMs: Long private var hashCode = uid.hashCode() @@ -473,8 +474,8 @@ class GenreImpl(grouping: Grouping, 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, musicSettings: MusicSett return this } } - -data class Grouping(var raw: PrioritizedRaw, val music: MutableList) - -data class PrioritizedRaw(val inner: R, val src: M) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 9b92046be..73fa3c753 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -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(var raw: PrioritizedRaw, val music: MutableSet) + +/** + * 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(val inner: R, val src: M) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 9d273ac98..9acbe82d0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index eb24d8093..d06c0ca37 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -286,7 +286,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) * * @param songs The [Song]s to share. */ -fun Context.share(songs: List) { +fun Context.share(songs: Collection) { if (songs.isEmpty()) return logD("Showing sharesheet for ${songs.size} songs") val builder = ShareCompat.IntentBuilder(this)