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:
parent
e687658874
commit
a4838cefaa
5 changed files with 59 additions and 34 deletions
|
@ -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
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -140,21 +140,28 @@ constructor(
|
||||||
|
|
||||||
private suspend fun openCoverInputStream(cover: Cover) =
|
private suspend fun openCoverInputStream(cover: Cover) =
|
||||||
try {
|
try {
|
||||||
when (imageSettings.coverMode) {
|
when (cover) {
|
||||||
CoverMode.OFF -> null
|
is Cover.Embedded ->
|
||||||
CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover)
|
when (imageSettings.coverMode) {
|
||||||
CoverMode.QUALITY -> extractQualityCover(cover)
|
CoverMode.OFF -> null
|
||||||
|
CoverMode.MEDIA_STORE -> extractMediaStoreCover(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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue