Merge branch 'media3' into dev

This commit is contained in:
Alexander Capehart 2024-04-29 11:10:03 -06:00
commit 0a3382cafd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 69 additions and 35 deletions

View file

@ -26,14 +26,14 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.service.IndexingServiceFragment import org.oxycblt.auxio.music.service.IndexerServiceFragment
import org.oxycblt.auxio.playback.service.MediaSessionServiceFragment import org.oxycblt.auxio.playback.service.MediaSessionServiceFragment
@AndroidEntryPoint @AndroidEntryPoint
class AuxioService : MediaLibraryService(), ForegroundListener { class AuxioService : MediaLibraryService(), ForegroundListener {
@Inject lateinit var mediaSessionFragment: MediaSessionServiceFragment @Inject lateinit var mediaSessionFragment: MediaSessionServiceFragment
@Inject lateinit var indexingFragment: IndexingServiceFragment @Inject lateinit var indexingFragment: IndexerServiceFragment
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
override fun onCreate() { override fun onCreate() {

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.perceptualHash }.hashCode()}" "${data.map { it.key }.hashCode()}"
} }
class CoverFetcher class CoverFetcher

View file

@ -22,23 +22,35 @@ import android.net.Uri
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
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 * The song has an embedded cover art we support, so we can operate with it on a per-song basis.
* an album cover. */
* @author Alexander Capehart (OxygenCobalt) data class Embedded(val songCoverUri: Uri, val songUri: Uri, val perceptualHash: String) :
*/ Cover {
data class Cover(val perceptualHash: String?, val mediaStoreUri: Uri, val songUri: Uri) { 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 {
private val FALLBACK_SORT = Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING) private val FALLBACK_SORT = Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING)
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.perceptualHash } .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,27 @@ 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 +172,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 +192,9 @@ 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

@ -64,7 +64,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
totalSize: Int, totalSize: Int,
msSinceStartScroll: Long msSinceStartScroll: Long
): Int { ): Int {
// Clamp the scroll speed to prevent thefrom freaking out // Clamp the scroll speed to prevent the lists from freaking out
// Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe // Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe
val standardSpeed = val standardSpeed =
super.interpolateOutOfBoundsScroll( super.interpolateOutOfBoundsScroll(

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
@ -114,7 +115,18 @@ class SongImpl(
get() = _genres get() = _genres
override val cover = override val cover =
Cover(rawSong.coverPerceptualHash, requireNotNull(rawSong.mediaStoreId).toCoverUri(), uri) rawSong.coverPerceptualHash?.let {
// We were able to confirm that the song had a parsable cover and can be used on
// a per-song basis. Otherwise, just fall back to a per-album cover instead, as
// it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not
// support the cover metadata of a given spec (unlikely).
Cover.Embedded(
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }
.toSongCoverUri(),
uri,
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

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* IndexerComponent.kt is part of Auxio. * IndexerServiceFragment.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
class IndexingServiceFragment class IndexerServiceFragment
@Inject @Inject
constructor( constructor(
@ApplicationContext override val workerContext: Context, @ApplicationContext override val workerContext: Context,

View file

@ -74,7 +74,7 @@ fun Song.toMediaItem(context: Context, parent: MusicParent?): MediaItem {
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
.setIsPlayable(true) .setIsPlayable(true)
.setIsBrowsable(false) .setIsBrowsable(false)
.setArtworkUri(album.cover.single.mediaStoreUri) .setArtworkUri(album.cover.single.mediaStoreCoverUri)
.setExtras( .setExtras(
Bundle().apply { Bundle().apply {
putString("uid", mediaSessionUID.toString()) putString("uid", mediaSessionUID.toString())
@ -105,7 +105,7 @@ fun Album.toMediaItem(context: Context): MediaItem {
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
.setIsPlayable(true) .setIsPlayable(true)
.setIsBrowsable(true) .setIsBrowsable(true)
.setArtworkUri(cover.single.mediaStoreUri) .setArtworkUri(cover.single.mediaStoreCoverUri)
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()
@ -136,7 +136,7 @@ fun Artist.toMediaItem(context: Context): MediaItem {
.setIsPlayable(true) .setIsPlayable(true)
.setIsBrowsable(true) .setIsBrowsable(true)
.setGenre(genres.resolveNames(context)) .setGenre(genres.resolveNames(context))
.setArtworkUri(cover.single.mediaStoreUri) .setArtworkUri(cover.single.mediaStoreCoverUri)
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()
@ -159,7 +159,7 @@ fun Genre.toMediaItem(context: Context): MediaItem {
.setMediaType(MediaMetadata.MEDIA_TYPE_GENRE) .setMediaType(MediaMetadata.MEDIA_TYPE_GENRE)
.setIsPlayable(true) .setIsPlayable(true)
.setIsBrowsable(true) .setIsBrowsable(true)
.setArtworkUri(cover.single.mediaStoreUri) .setArtworkUri(cover.single.mediaStoreCoverUri)
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()
@ -182,7 +182,7 @@ fun Playlist.toMediaItem(context: Context): MediaItem {
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
.setIsPlayable(true) .setIsPlayable(true)
.setIsBrowsable(true) .setIsBrowsable(true)
.setArtworkUri(cover?.single?.mediaStoreUri) .setArtworkUri(cover?.single?.mediaStoreCoverUri)
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) }) .setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()

2
media

@ -1 +1 @@
Subproject commit 6c77cfa13c83bf2ae5188603d2c9a51ec4cb3ac3 Subproject commit 1d58171e16107d73ec3c842319663a8a06bfd23a