Merge branch 'media3' into dev
This commit is contained in:
commit
0a3382cafd
10 changed files with 69 additions and 35 deletions
|
@ -26,14 +26,14 @@ import androidx.media3.session.MediaLibraryService
|
|||
import androidx.media3.session.MediaSession
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AuxioService : MediaLibraryService(), ForegroundListener {
|
||||
@Inject lateinit var mediaSessionFragment: MediaSessionServiceFragment
|
||||
|
||||
@Inject lateinit var indexingFragment: IndexingServiceFragment
|
||||
@Inject lateinit var indexingFragment: IndexerServiceFragment
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
override fun onCreate() {
|
||||
|
|
|
@ -27,7 +27,7 @@ import javax.inject.Inject
|
|||
|
||||
class CoverKeyer @Inject constructor() : Keyer<Collection<Cover>> {
|
||||
override fun key(data: Collection<Cover>, options: Options) =
|
||||
"${data.map { it.perceptualHash }.hashCode()}"
|
||||
"${data.map { it.key }.hashCode()}"
|
||||
}
|
||||
|
||||
class CoverFetcher
|
||||
|
|
|
@ -22,23 +22,35 @@ import android.net.Uri
|
|||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading
|
||||
* images.
|
||||
*
|
||||
* @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 perceptualHash: String?, val mediaStoreUri: Uri, val songUri: Uri) {
|
||||
sealed interface Cover {
|
||||
val key: String
|
||||
val mediaStoreCoverUri: Uri
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private val FALLBACK_SORT = Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING)
|
||||
|
||||
fun order(songs: Collection<Song>) =
|
||||
FALLBACK_SORT.songs(songs)
|
||||
.map { it.cover }
|
||||
.groupBy { it.perceptualHash }
|
||||
.groupBy { it.key }
|
||||
.entries
|
||||
.sortedByDescending { it.value.size }
|
||||
.map { it.value.first() }
|
||||
|
|
|
@ -140,21 +140,27 @@ constructor(
|
|||
|
||||
private suspend fun openCoverInputStream(cover: Cover) =
|
||||
try {
|
||||
when (imageSettings.coverMode) {
|
||||
CoverMode.OFF -> null
|
||||
CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover)
|
||||
CoverMode.QUALITY -> extractQualityCover(cover)
|
||||
when (cover) {
|
||||
is Cover.Embedded ->
|
||||
when (imageSettings.coverMode) {
|
||||
CoverMode.OFF -> null
|
||||
CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover)
|
||||
CoverMode.QUALITY -> extractQualityCover(cover)
|
||||
}
|
||||
is Cover.External -> {
|
||||
extractMediaStoreCover(cover)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to extract album cover due to an error: $e")
|
||||
null
|
||||
}
|
||||
|
||||
private suspend fun extractQualityCover(cover: Cover) =
|
||||
private suspend fun extractQualityCover(cover: Cover.Embedded) =
|
||||
extractAospMetadataCover(cover)
|
||||
?: extractExoplayerCover(cover) ?: extractMediaStoreCover(cover)
|
||||
|
||||
private fun extractAospMetadataCover(cover: Cover): InputStream? =
|
||||
private fun extractAospMetadataCover(cover: Cover.Embedded): 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
|
||||
|
@ -166,7 +172,7 @@ constructor(
|
|||
embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
||||
}
|
||||
|
||||
private suspend fun extractExoplayerCover(cover: Cover): InputStream? {
|
||||
private suspend fun extractExoplayerCover(cover: Cover.Embedded): InputStream? {
|
||||
val tracks =
|
||||
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(cover.songUri))
|
||||
.asDeferred()
|
||||
|
@ -186,7 +192,9 @@ constructor(
|
|||
|
||||
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(cover.mediaStoreUri) }
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(cover.mediaStoreCoverUri)
|
||||
}
|
||||
|
||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||
|
|
|
@ -64,7 +64,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
|
|||
totalSize: Int,
|
||||
msSinceStartScroll: Long
|
||||
): 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
|
||||
val standardSpeed =
|
||||
super.interpolateOutOfBoundsScroll(
|
||||
|
|
|
@ -29,8 +29,9 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
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.toCoverUri
|
||||
import org.oxycblt.auxio.music.fs.toSongCoverUri
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
|
@ -114,7 +115,18 @@ class SongImpl(
|
|||
get() = _genres
|
||||
|
||||
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
|
||||
|
|
|
@ -102,13 +102,15 @@ fun Long.toAudioUri() =
|
|||
* @return An external storage image [Uri]. May not exist.
|
||||
* @see ContentUris.withAppendedId
|
||||
*/
|
||||
fun Long.toCoverUri(): Uri =
|
||||
fun Long.toSongCoverUri(): Uri =
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run {
|
||||
appendPath(this@toCoverUri.toString())
|
||||
appendPath(this@toSongCoverUri.toString())
|
||||
appendPath("albumart")
|
||||
build()
|
||||
}
|
||||
|
||||
fun Long.toAlbumCoverUri(): Uri = ContentUris.withAppendedId(externalCoversUri, this)
|
||||
|
||||
// --- STORAGEMANAGER UTILITIES ---
|
||||
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* 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
|
||||
* 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.logD
|
||||
|
||||
class IndexingServiceFragment
|
||||
class IndexerServiceFragment
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext override val workerContext: Context,
|
|
@ -74,7 +74,7 @@ fun Song.toMediaItem(context: Context, parent: MusicParent?): MediaItem {
|
|||
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
|
||||
.setIsPlayable(true)
|
||||
.setIsBrowsable(false)
|
||||
.setArtworkUri(album.cover.single.mediaStoreUri)
|
||||
.setArtworkUri(album.cover.single.mediaStoreCoverUri)
|
||||
.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(cover.single.mediaStoreUri)
|
||||
.setArtworkUri(cover.single.mediaStoreCoverUri)
|
||||
.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(cover.single.mediaStoreUri)
|
||||
.setArtworkUri(cover.single.mediaStoreCoverUri)
|
||||
.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(cover.single.mediaStoreUri)
|
||||
.setArtworkUri(cover.single.mediaStoreCoverUri)
|
||||
.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(cover?.single?.mediaStoreUri)
|
||||
.setArtworkUri(cover?.single?.mediaStoreCoverUri)
|
||||
.setExtras(Bundle().apply { putString("uid", mediaSessionUID.toString()) })
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
|
|
2
media
2
media
|
@ -1 +1 @@
|
|||
Subproject commit 6c77cfa13c83bf2ae5188603d2c9a51ec4cb3ac3
|
||||
Subproject commit 1d58171e16107d73ec3c842319663a8a06bfd23a
|
Loading…
Reference in a new issue