image: basic per-song album covers
Without any good caching support, so this will immediately break down.
This commit is contained in:
parent
8b7b916489
commit
bd330f0c71
7 changed files with 58 additions and 47 deletions
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* CoverUri.kt is part of Auxio.
|
||||
* Cover.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
|
||||
|
@ -24,9 +24,9 @@ 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 mediaStoreUri 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)
|
||||
data class Cover(val perceptualHash: String?, val mediaStoreUri: Uri, val songUri: Uri)
|
|
@ -77,10 +77,10 @@ constructor(
|
|||
* [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned.
|
||||
*/
|
||||
suspend fun extract(songs: Collection<Song>, size: Size): FetchResult? {
|
||||
val albums = computeCoverOrdering(songs)
|
||||
val covers = computeCoverOrdering(songs)
|
||||
val streams = mutableListOf<InputStream>()
|
||||
for (album in albums) {
|
||||
openCoverInputStream(album)?.let(streams::add)
|
||||
for (cover in covers) {
|
||||
openCoverInputStream(cover)?.let(streams::add)
|
||||
// We don't immediately check for mosaic feasibility from album count alone, as that
|
||||
// does not factor in InputStreams failing to load. Instead, only check once we
|
||||
// definitely have image data to use.
|
||||
|
@ -116,40 +116,33 @@ 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: 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)
|
||||
fun computeCoverOrdering(songs: Collection<Song>) =
|
||||
Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING)
|
||||
.songs(songs)
|
||||
.distinctBy { (it.cover.perceptualHash ?: it.uri).toString() }
|
||||
.map { it.cover }
|
||||
|
||||
val sortedMap =
|
||||
sortedMapOf<Album, Int>(Sort.Mode.ByName.getAlbumComparator(Sort.Direction.ASCENDING))
|
||||
for (song in songs) {
|
||||
sortedMap[song.album] = (sortedMap[song.album] ?: 0) + 1
|
||||
}
|
||||
return sortedMap.keys.sortedByDescending { sortedMap[it] }
|
||||
}
|
||||
|
||||
private suspend fun openCoverInputStream(album: Album) =
|
||||
private suspend fun openCoverInputStream(cover: Cover) =
|
||||
try {
|
||||
when (imageSettings.coverMode) {
|
||||
CoverMode.OFF -> null
|
||||
CoverMode.MEDIA_STORE -> extractMediaStoreCover(album)
|
||||
CoverMode.QUALITY -> extractQualityCover(album)
|
||||
CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover)
|
||||
CoverMode.QUALITY -> extractQualityCover(cover)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to extract album cover due to an error: $e")
|
||||
null
|
||||
}
|
||||
|
||||
private suspend fun extractQualityCover(album: Album) =
|
||||
extractAospMetadataCover(album)
|
||||
?: extractExoplayerCover(album) ?: extractMediaStoreCover(album)
|
||||
private suspend fun extractQualityCover(cover: Cover) =
|
||||
extractAospMetadataCover(cover)
|
||||
?: extractExoplayerCover(cover) ?: extractMediaStoreCover(cover)
|
||||
|
||||
private fun extractAospMetadataCover(album: Album): InputStream? =
|
||||
private fun extractAospMetadataCover(cover: Cover): InputStream? =
|
||||
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.coverUri.song)
|
||||
setDataSource(context, cover.songUri)
|
||||
|
||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||
// ByteArray of the cover without any compression artifacts.
|
||||
|
@ -157,10 +150,9 @@ constructor(
|
|||
embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
||||
}
|
||||
|
||||
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
||||
private suspend fun extractExoplayerCover(cover: Cover): InputStream? {
|
||||
val tracks =
|
||||
MetadataRetriever.retrieveMetadata(
|
||||
mediaSourceFactory, MediaItem.fromUri(album.coverUri.song))
|
||||
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(cover.songUri))
|
||||
.asDeferred()
|
||||
.await()
|
||||
|
||||
|
@ -204,11 +196,9 @@ constructor(
|
|||
return stream
|
||||
}
|
||||
|
||||
private suspend fun extractMediaStoreCover(album: Album) =
|
||||
private suspend fun extractMediaStoreCover(cover: Cover) =
|
||||
// Eliminate any chance that this blocking call might mess up the loading process
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(album.coverUri.mediaStore)
|
||||
}
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(cover.mediaStoreUri) }
|
||||
|
||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||
|
|
|
@ -27,7 +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.image.extractor.Cover
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
|
@ -246,6 +246,8 @@ interface Song : Music {
|
|||
* audio file in a way that is scoped-storage-safe.
|
||||
*/
|
||||
val uri: Uri
|
||||
/** Useful information to quickly obtain the album cover. */
|
||||
val cover: Cover
|
||||
/**
|
||||
* The [Path] to this audio file. This is only intended for display, [uri] should be favored
|
||||
* instead for accessing the audio file.
|
||||
|
@ -293,11 +295,8 @@ interface Album : MusicParent {
|
|||
* [ReleaseType.Album].
|
||||
*/
|
||||
val releaseType: ReleaseType
|
||||
/**
|
||||
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
|
||||
* cost of image quality.
|
||||
*/
|
||||
val coverUri: CoverUri
|
||||
/** Cover information from the template song used for the album. */
|
||||
val cover: Cover
|
||||
/** 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. */
|
||||
|
@ -326,6 +325,8 @@ interface Artist : MusicParent {
|
|||
* songs.
|
||||
*/
|
||||
val durationMs: Long?
|
||||
/** Useful information to quickly obtain a (single) cover for a Genre. */
|
||||
val cover: Cover
|
||||
/** The [Genre]s of this artist. */
|
||||
val genres: List<Genre>
|
||||
}
|
||||
|
@ -340,6 +341,8 @@ interface Genre : MusicParent {
|
|||
val artists: Collection<Artist>
|
||||
/** The total duration of the songs in this genre, in milliseconds. */
|
||||
val durationMs: Long
|
||||
/** Useful information to quickly obtain a (single) cover for a Genre. */
|
||||
val cover: Cover
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -352,6 +355,8 @@ interface Playlist : MusicParent {
|
|||
override val songs: List<Song>
|
||||
/** The total duration of the songs in this genre, in milliseconds. */
|
||||
val durationMs: Long
|
||||
/** Useful information to quickly obtain a (single) cover for a Genre. */
|
||||
val cover: Cover?
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.image.extractor.CoverUri
|
||||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -112,6 +112,8 @@ class SongImpl(
|
|||
override val genres: List<Genre>
|
||||
get() = _genres
|
||||
|
||||
override val cover = Cover("", requireNotNull(rawSong.mediaStoreId).toCoverUri(), uri)
|
||||
|
||||
/**
|
||||
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
|
||||
* [Album].
|
||||
|
@ -291,9 +293,9 @@ class AlbumImpl(
|
|||
override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName)
|
||||
override val dates: Date.Range?
|
||||
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
||||
override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
|
||||
override val durationMs: Long
|
||||
override val dateAdded: Long
|
||||
override val cover = grouping.raw.src.cover
|
||||
|
||||
private val _artists = mutableListOf<ArtistImpl>()
|
||||
override val artists: List<Artist>
|
||||
|
@ -419,6 +421,12 @@ class ArtistImpl(
|
|||
override val explicitAlbums: Set<Album>
|
||||
override val implicitAlbums: Set<Album>
|
||||
override val durationMs: Long?
|
||||
override val cover =
|
||||
when (val src = grouping.raw.src) {
|
||||
is AlbumImpl -> src.cover
|
||||
is SongImpl -> src.cover
|
||||
else -> error("Unexpected input music $src in $name ${src::class.simpleName}")
|
||||
}
|
||||
|
||||
override lateinit var genres: List<Genre>
|
||||
|
||||
|
@ -528,6 +536,7 @@ class GenreImpl(
|
|||
override val songs: Set<Song>
|
||||
override val artists: Set<Artist>
|
||||
override val durationMs: Long
|
||||
override val cover = grouping.raw.src.cover
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
|
||||
|
|
|
@ -102,7 +102,12 @@ fun Long.toAudioUri() =
|
|||
* @return An external storage image [Uri]. May not exist.
|
||||
* @see ContentUris.withAppendedId
|
||||
*/
|
||||
fun Long.toCoverUri() = ContentUris.withAppendedId(externalCoversUri, this)
|
||||
fun Long.toCoverUri(): Uri =
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run {
|
||||
appendPath(this@toCoverUri.toString())
|
||||
appendPath("albumart")
|
||||
build()
|
||||
}
|
||||
|
||||
// --- STORAGEMANAGER UTILITIES ---
|
||||
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||
|
|
|
@ -74,7 +74,7 @@ fun Song.toMediaItem(context: Context, parent: MusicParent?): MediaItem {
|
|||
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
|
||||
.setIsPlayable(true)
|
||||
.setIsBrowsable(false)
|
||||
.setArtworkUri(album.coverUri.mediaStore)
|
||||
.setArtworkUri(album.cover.mediaStoreUri)
|
||||
.setExtras(
|
||||
Bundle().apply {
|
||||
putString("uid", mediaSessionUID.toString())
|
||||
|
@ -105,7 +105,7 @@ fun Album.toMediaItem(context: Context): MediaItem {
|
|||
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
|
||||
.setIsPlayable(true)
|
||||
.setIsBrowsable(true)
|
||||
.setArtworkUri(coverUri.mediaStore)
|
||||
.setArtworkUri(cover.mediaStoreUri)
|
||||
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
|
@ -136,7 +136,7 @@ fun Artist.toMediaItem(context: Context): MediaItem {
|
|||
.setIsPlayable(true)
|
||||
.setIsBrowsable(true)
|
||||
.setGenre(genres.resolveNames(context))
|
||||
.setArtworkUri(songs.firstOrNull()?.album?.coverUri?.mediaStore)
|
||||
.setArtworkUri(cover.mediaStoreUri)
|
||||
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
|
@ -159,7 +159,7 @@ fun Genre.toMediaItem(context: Context): MediaItem {
|
|||
.setMediaType(MediaMetadata.MEDIA_TYPE_GENRE)
|
||||
.setIsPlayable(true)
|
||||
.setIsBrowsable(true)
|
||||
.setArtworkUri(songs.firstOrNull()?.album?.coverUri?.mediaStore)
|
||||
.setArtworkUri(cover.mediaStoreUri)
|
||||
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
|
@ -182,7 +182,7 @@ fun Playlist.toMediaItem(context: Context): MediaItem {
|
|||
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
|
||||
.setIsPlayable(true)
|
||||
.setIsBrowsable(true)
|
||||
.setArtworkUri(songs.firstOrNull()?.album?.coverUri?.mediaStore)
|
||||
.setArtworkUri(cover?.mediaStoreUri)
|
||||
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
|
|
|
@ -46,6 +46,8 @@ private constructor(
|
|||
|
||||
override fun toString() = "Playlist(uid=$uid, name=$name)"
|
||||
|
||||
override val cover = songs.firstOrNull()?.cover
|
||||
|
||||
/**
|
||||
* Clone the data in this instance to a new [PlaylistImpl] with the given [name].
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue