image: properly differentiate cover types

- If we could find an embedded cover, then we can treat it as a
per-song cover
- Otherwise, just do our old album-based behavior.
This commit is contained in:
Alexander Capehart 2024-04-22 10:44:03 -06:00
parent e687658874
commit a4838cefaa
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 59 additions and 34 deletions

View file

@ -27,7 +27,7 @@ import javax.inject.Inject
class CoverKeyer @Inject constructor() : Keyer<Collection<Cover>> { class CoverKeyer @Inject constructor() : Keyer<Collection<Cover>> {
override fun key(data: Collection<Cover>, options: Options) = override fun key(data: Collection<Cover>, options: Options) =
"${data.map { it.uniqueness }.hashCode()}" "${data.map { it.key }.hashCode()}"
} }
class CoverFetcher class CoverFetcher

View file

@ -20,23 +20,28 @@ package org.oxycblt.auxio.image.extractor
import android.net.Uri import android.net.Uri
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
/** sealed interface Cover {
* Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading val key: String
* images. val mediaStoreCoverUri: Uri
*
* @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 Cover(val uniqueness: Uniqueness?, val mediaStoreUri: Uri, val songUri: Uri) {
sealed interface Uniqueness {
data class PerceptualHash(val perceptualHash: String) : Uniqueness
data class UID(val uid: Music.UID) : Uniqueness /**
* The song has an embedded cover art we support, so we can operate with it on a per-song
* basis.
*/
data class Embedded(val songCoverUri: Uri, val songUri: Uri, val perceptualHash: String) : Cover {
override val mediaStoreCoverUri = songCoverUri
override val key = perceptualHash
}
/**
* We couldn't find any embedded cover art ourselves, but the android system might have some
* through a cover.jpg file or something similar.
*/
data class External(val albumCoverUri: Uri) : Cover {
override val mediaStoreCoverUri = albumCoverUri
override val key = albumCoverUri.toString()
} }
companion object { companion object {
@ -45,7 +50,7 @@ data class Cover(val uniqueness: Uniqueness?, val mediaStoreUri: Uri, val songUr
fun order(songs: Collection<Song>) = fun order(songs: Collection<Song>) =
FALLBACK_SORT.songs(songs) FALLBACK_SORT.songs(songs)
.map { it.cover } .map { it.cover }
.groupBy { it.uniqueness } .groupBy { it.key }
.entries .entries
.sortedByDescending { it.value.size } .sortedByDescending { it.value.size }
.map { it.value.first() } .map { it.value.first() }

View file

@ -140,21 +140,28 @@ constructor(
private suspend fun openCoverInputStream(cover: Cover) = private suspend fun openCoverInputStream(cover: Cover) =
try { try {
when (cover) {
is Cover.Embedded ->
when (imageSettings.coverMode) { when (imageSettings.coverMode) {
CoverMode.OFF -> null CoverMode.OFF -> null
CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover) CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover)
CoverMode.QUALITY -> extractQualityCover(cover) CoverMode.QUALITY -> extractQualityCover(cover)
} }
is Cover.External -> {
extractMediaStoreCover(cover)
}
}
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to extract album cover due to an error: $e") logE("Unable to extract album cover due to an error: $e")
null null
} }
private suspend fun extractQualityCover(cover: Cover) = private suspend fun extractQualityCover(cover: Cover.Embedded) =
extractAospMetadataCover(cover) extractAospMetadataCover(cover)
?: extractExoplayerCover(cover) ?: extractMediaStoreCover(cover) ?: extractExoplayerCover(cover) ?: extractMediaStoreCover(cover)
private fun extractAospMetadataCover(cover: Cover): InputStream? = private fun extractAospMetadataCover(cover: Cover.Embedded): InputStream? =
MediaMetadataRetriever().run { MediaMetadataRetriever().run {
// This call is time-consuming but it also doesn't seem to hold up the main thread, // 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 // so it's probably fine not to wrap it.rmt
@ -166,7 +173,7 @@ constructor(
embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
} }
private suspend fun extractExoplayerCover(cover: Cover): InputStream? { private suspend fun extractExoplayerCover(cover: Cover.Embedded): InputStream? {
val tracks = val tracks =
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(cover.songUri)) MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(cover.songUri))
.asDeferred() .asDeferred()
@ -186,7 +193,7 @@ constructor(
private suspend fun extractMediaStoreCover(cover: Cover) = private suspend fun extractMediaStoreCover(cover: Cover) =
// Eliminate any chance that this blocking call might mess up the loading process // Eliminate any chance that this blocking call might mess up the loading process
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(cover.mediaStoreUri) } withContext(Dispatchers.IO) { context.contentResolver.openInputStream(cover.mediaStoreCoverUri) }
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult { private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {

View file

@ -29,8 +29,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.toAlbumCoverUri
import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.fs.toAudioUri
import org.oxycblt.auxio.music.fs.toCoverUri import org.oxycblt.auxio.music.fs.toSongCoverUri
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
@ -76,7 +77,8 @@ class SongImpl(
override val name = override val name =
nameFactory.parse( nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" }, requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" },
rawSong.sortName) rawSong.sortName
)
override val track = rawSong.track override val track = rawSong.track
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
@ -114,11 +116,20 @@ class SongImpl(
get() = _genres get() = _genres
override val cover = override val cover =
Cover( rawSong.coverPerceptualHash?.let {
rawSong.coverPerceptualHash?.let { Cover.Uniqueness.PerceptualHash(it) } // We were able to confirm that the song had a parsable cover and can be used on
?: Cover.Uniqueness.UID(uid), // a per-song basis. Otherwise, just fall back to a per-album cover instead, as
requireNotNull(rawSong.mediaStoreId).toCoverUri(), // it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not
uri) // support the cover metadata of a given spec (unlikely).
Cover.Embedded(
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toSongCoverUri(),
uid,
it
)
}
?: Cover.External(
requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri()
)
/** /**
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an

View file

@ -102,13 +102,15 @@ fun Long.toAudioUri() =
* @return An external storage image [Uri]. May not exist. * @return An external storage image [Uri]. May not exist.
* @see ContentUris.withAppendedId * @see ContentUris.withAppendedId
*/ */
fun Long.toCoverUri(): Uri = fun Long.toSongCoverUri(): Uri =
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run { MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run {
appendPath(this@toCoverUri.toString()) appendPath(this@toSongCoverUri.toString())
appendPath("albumart") appendPath("albumart")
build() build()
} }
fun Long.toAlbumCoverUri(): Uri = ContentUris.withAppendedId(externalCoversUri, this)
// --- STORAGEMANAGER UTILITIES --- // --- STORAGEMANAGER UTILITIES ---
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles // Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles