From 189f712eaa8a727f1ce917c619a05f1a74e9ca3b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 9 Sep 2022 19:54:37 -0600 Subject: [PATCH] music: re-add mediastore genre parsing Re-add MediaStore genre parsing to the init() step. This time, to play along with the new abstraction, the query is done in initialization and loaded into a map that is then used when populating raw audio. --- .../auxio/music/extractor/MediaStoreLayer.kt | 58 ++++++++++++++++++- .../org/oxycblt/auxio/music/system/Indexer.kt | 2 + 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt index 388f2ebda..8c691f2a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.directoryCompat import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.queryCursor import org.oxycblt.auxio.music.storageVolumesCompat +import org.oxycblt.auxio.music.useQuery import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.getSystemServiceCompat @@ -120,6 +121,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa private var albumArtistIndex = -1 private val settings = Settings(context) + private val genreNamesMap = mutableMapOf>() private val _volumes = mutableListOf() protected val volumes: List @@ -127,6 +129,9 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa /** Initialize this instance by making a query over the media database. */ open fun init(): Cursor { + logD("Initializing") + val start = System.currentTimeMillis() + cacheLayer.init() val storageManager = context.getSystemServiceCompat(StorageManager::class) @@ -163,7 +168,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa selector += ')' } - logD("Starting query [proj: ${projection.toList()}, selector: $selector, args: $args]") + logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]") val cursor = requireNotNull( @@ -191,6 +196,46 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) + logD("Song query succeeded [Projected total: ${cursor.count}]") + + logD("Assembling genre map") + + // Since we can't obtain the genre tag from a song query, we must construct + // our own equivalent from genre database queries. Theoretically, this isn't + // needed since MetadataLayer will fill this in for us, but there are some + // obscure formats where genre support is only really covered by this. + context.contentResolverSafe.useQuery( + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME) + ) { genreCursor -> + val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) + val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) + + while (genreCursor.moveToNext()) { + // We can't assume what format these genres are derived from, so we just have + // to assume ID3v2 rules. + val id = genreCursor.getLong(idIndex) + val names = (genreCursor.getStringOrNull(nameIndex) ?: continue) + .parseId3GenreNames(settings) + + context.contentResolverSafe.useQuery( + MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), + arrayOf(MediaStore.Audio.Genres.Members._ID) + ) { cursor -> + val songIdIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) + + while (cursor.moveToNext()) { + // Assume that a song can't inhabit multiple genre entries, as I doubt + // Android is smart enough to separate genres into separators. + genreNamesMap[cursor.getLong(songIdIndex)] = names + } + } + } + } + + logD("Finished initialization in ${System.currentTimeMillis() - start}ms") + return cursor } @@ -221,7 +266,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa raw.dateModified = cursor.getLong(dateAddedIndex) if (cacheLayer.maybePopulateCachedRaw(raw)) { - // We found a valid cache entry, no need to build it. + // We found a valid cache entry, no need to extract metadata. logD("Found cached raw: ${raw.name}") return true } @@ -301,6 +346,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa // The album artist field is nullable and never has placeholder values. raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings) + + raw.genreNames = genreNamesMap[raw.mediaStoreId] } companion object { @@ -313,6 +360,13 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa @Suppress("InlinedApi") private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST + /** + * External has existed since at least API 21, but no constant existed for it until API 29. + * This constant is safe to use. + */ + @Suppress("InlinedApi") + private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL + /** * The base selector that works across all versions of android. Does not exclude * directories. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index c1c7a9ece..fbe4b142f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -62,6 +62,8 @@ import org.oxycblt.auxio.util.logW * directly work with music loading, making such redundant. * * @author OxygenCobalt + * + * TODO: Try to replace TaskGuard with yield when possible */ class Indexer { private var lastResponse: Response? = null