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)