music: refine new mediastoreextractor impl

- Make the interpreters use a more conventional naming structure
- Remove the redundant file name extraction that is largely an artifact
of older versions
This commit is contained in:
Alexander Capehart 2024-01-01 11:59:51 -07:00
parent 6b9f6862af
commit ed519eeccc
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 70 additions and 78 deletions

View file

@ -74,37 +74,31 @@ class SongImpl(
} }
override val name = override val name =
nameFactory.parse( nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.fileName}: 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) }
override val date = rawSong.date override val date = rawSong.date
override val uri = override val uri =
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" } requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri()
.toAudioUri() override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" }
override val path =
requireNotNull(rawSong.directory) { "Invalid raw ${rawSong.fileName}: No parent directory" }
.file(
requireNotNull(rawSong.fileName) {
"Invalid raw ${rawSong.fileName}: No display name"
})
override val mimeType = override val mimeType =
MimeType( MimeType(
fromExtension = fromExtension =
requireNotNull(rawSong.extensionMimeType) { requireNotNull(rawSong.extensionMimeType) {
"Invalid raw ${rawSong.fileName}: No mime type" "Invalid raw ${rawSong.path}: No mime type"
}, },
fromFormat = null) fromFormat = null)
override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.fileName}: No size" } override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" }
override val durationMs = override val durationMs =
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.fileName}: No duration" } requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" }
override val replayGainAdjustment = override val replayGainAdjustment =
ReplayGainAdjustment( ReplayGainAdjustment(
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
override val dateAdded = override val dateAdded =
requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.fileName}: No date added" } requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" }
private var _album: AlbumImpl? = null private var _album: AlbumImpl? = null
override val album: Album override val album: Album
@ -170,12 +164,12 @@ class SongImpl(
RawAlbum( RawAlbum(
mediaStoreId = mediaStoreId =
requireNotNull(rawSong.albumMediaStoreId) { requireNotNull(rawSong.albumMediaStoreId) {
"Invalid raw ${rawSong.fileName}: No album id" "Invalid raw ${rawSong.path}: No album id"
}, },
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
name = name =
requireNotNull(rawSong.albumName) { requireNotNull(rawSong.albumName) {
"Invalid raw ${rawSong.fileName}: No album name" "Invalid raw ${rawSong.path}: No album name"
}, },
sortName = rawSong.albumSortName, sortName = rawSong.albumSortName,
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)), releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),

View file

@ -42,9 +42,7 @@ data class RawSong(
/** The latest date the [SongImpl]'s audio file was modified, as a unix epoch timestamp. */ /** The latest date the [SongImpl]'s audio file was modified, as a unix epoch timestamp. */
var dateModified: Long? = null, var dateModified: Long? = null,
/** @see Song.path */ /** @see Song.path */
var fileName: String? = null, var path: Path? = null,
/** @see Song.path */
var directory: Path? = null,
/** @see Song.size */ /** @see Song.size */
var size: Long? = null, var size: Long? = null,
/** @see Song.durationMs */ /** @see Song.durationMs */

View file

@ -24,7 +24,6 @@ import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import java.io.File
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.Cache import org.oxycblt.auxio.music.cache.Cache
@ -119,8 +118,8 @@ interface MediaStoreExtractor {
private class MediaStoreExtractorImpl( private class MediaStoreExtractorImpl(
private val context: Context, private val context: Context,
private val pathInterpreterFactory: PathInterpreterFactory, private val pathInterpreterFactory: PathInterpreter.Factory,
private val tagInterpreterFactory: TagInterpreterFactory private val tagInterpreterFactory: TagInterpreter.Factory
) : MediaStoreExtractor { ) : MediaStoreExtractor {
override suspend fun query( override suspend fun query(
constraints: MediaStoreExtractor.Constraints constraints: MediaStoreExtractor.Constraints
@ -239,8 +238,6 @@ private class MediaStoreExtractorImpl(
) : MediaStoreExtractor.Query { ) : MediaStoreExtractor.Query {
private val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) private val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
private val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) private val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
private val displayNameIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
private val mimeTypeIndex = private val mimeTypeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
private val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE) private val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
@ -267,9 +264,6 @@ private class MediaStoreExtractorImpl(
rawSong.mediaStoreId = cursor.getLong(idIndex) rawSong.mediaStoreId = cursor.getLong(idIndex)
rawSong.dateAdded = cursor.getLong(dateAddedIndex) rawSong.dateAdded = cursor.getLong(dateAddedIndex)
rawSong.dateModified = cursor.getLong(dateModifiedIndex) rawSong.dateModified = cursor.getLong(dateModifiedIndex)
// Try to use the DISPLAY_NAME column to obtain a (probably sane) file name
// from the android system.
rawSong.fileName = cursor.getStringOrNull(displayNameIndex)
rawSong.extensionMimeType = cursor.getString(mimeTypeIndex) rawSong.extensionMimeType = cursor.getString(mimeTypeIndex)
rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex) rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex)
pathInterpreter.populate(rawSong) pathInterpreter.populate(rawSong)
@ -289,7 +283,8 @@ private class MediaStoreExtractorImpl(
// A non-existent album name should theoretically be the name of the folder it contained // A non-existent album name should theoretically be the name of the folder it contained
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it // in, but in practice it is more often "0" (as in /storage/emulated/0), even when it
// the file is not actually in the root internal storage directory. We can't do // the file is not actually in the root internal storage directory. We can't do
// anything to fix this, really. // anything to fix this, really. We also can't really filter it out, since how can we
// know when it corresponds to the folder and not, say, Low Roar's breakout album "0"?
rawSong.albumName = cursor.getString(albumIndex) rawSong.albumName = cursor.getString(albumIndex)
// Android does not make a non-existent artist tag null, it instead fills it in // Android does not make a non-existent artist tag null, it instead fills it in
// as <unknown>, which makes absolutely no sense given how other columns default // as <unknown>, which makes absolutely no sense given how other columns default
@ -336,7 +331,6 @@ private class MediaStoreExtractorImpl(
MediaStore.Audio.AudioColumns._ID, MediaStore.Audio.AudioColumns._ID,
MediaStore.Audio.AudioColumns.DATE_ADDED, MediaStore.Audio.AudioColumns.DATE_ADDED,
MediaStore.Audio.AudioColumns.DATE_MODIFIED, MediaStore.Audio.AudioColumns.DATE_MODIFIED,
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
MediaStore.Audio.AudioColumns.SIZE, MediaStore.Audio.AudioColumns.SIZE,
MediaStore.Audio.AudioColumns.DURATION, MediaStore.Audio.AudioColumns.DURATION,
MediaStore.Audio.AudioColumns.MIME_TYPE, MediaStore.Audio.AudioColumns.MIME_TYPE,
@ -349,62 +343,54 @@ private class MediaStoreExtractorImpl(
} }
} }
interface Interpreter { private interface Interpreter {
fun populate(rawSong: RawSong) fun populate(rawSong: RawSong)
interface Factory {
val projection: Array<String>
fun wrap(cursor: Cursor): Interpreter
}
} }
interface InterpreterFactory { private sealed interface PathInterpreter : Interpreter {
val projection: Array<String> interface Factory : Interpreter.Factory {
override fun wrap(cursor: Cursor): PathInterpreter
fun wrap(cursor: Cursor): Interpreter fun createSelector(paths: List<Path>): Selector?
data class Selector(val template: String, val args: List<String>)
}
} }
interface PathInterpreterFactory : InterpreterFactory { private class DataPathInterpreter(
override fun wrap(cursor: Cursor): PathInterpreter private val cursor: Cursor,
private val volumeManager: VolumeManager
fun createSelector(paths: List<Path>): Selector? ) : PathInterpreter {
data class Selector(val template: String, val args: List<String>)
}
interface TagInterpreterFactory : InterpreterFactory {
override fun wrap(cursor: Cursor): TagInterpreter
}
sealed interface PathInterpreter : Interpreter
class DataPathInterpreter(private val cursor: Cursor, private val volumeManager: VolumeManager) :
PathInterpreter {
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
private val volumes = volumeManager.getVolumes() private val volumes = volumeManager.getVolumes()
override fun populate(rawSong: RawSong) { override fun populate(rawSong: RawSong) {
val data = cursor.getString(dataIndex) val data = Components.parseUnix(cursor.getString(dataIndex))
// On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume
// that this only applies to below API 29, as beyond API 29, this column not being
// present would completely break the scoped storage system. Fill it in with DATA
// if it's not available.
if (rawSong.fileName == null) {
rawSong.fileName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
}
// Find the volume that transforms the DATA column into a relative path. This is // Find the volume that transforms the DATA column into a relative path. This is
// the Directory we will use. // the Directory we will use.
val rawPath = Components.parseUnix(data)
for (volume in volumes) { for (volume in volumes) {
val volumePath = volume.components ?: continue val volumePath = volume.components ?: continue
if (volumePath.contains(rawPath)) { if (volumePath.contains(data)) {
rawSong.directory = Path(volume, rawPath.depth(volumePath.components.size)) rawSong.path = Path(volume, data.depth(volumePath.components.size))
break break
} }
} }
} }
class Factory(private val volumeManager: VolumeManager) : PathInterpreterFactory { class Factory(private val volumeManager: VolumeManager) : PathInterpreter.Factory {
override val projection: Array<String> override val projection: Array<String>
get() = arrayOf(MediaStore.Audio.AudioColumns.DATA) get() =
arrayOf(
MediaStore.Audio.AudioColumns.DISPLAY_NAME, MediaStore.Audio.AudioColumns.DATA)
override fun createSelector(paths: List<Path>): PathInterpreterFactory.Selector? { override fun createSelector(paths: List<Path>): PathInterpreter.Factory.Selector? {
val args = mutableListOf<String>() val args = mutableListOf<String>()
var template = "" var template = ""
for (i in paths.indices) { for (i in paths.indices) {
@ -423,7 +409,7 @@ class DataPathInterpreter(private val cursor: Cursor, private val volumeManager:
return null return null
} }
return PathInterpreterFactory.Selector(template, args) return PathInterpreter.Factory.Selector(template, args)
} }
override fun wrap(cursor: Cursor): PathInterpreter = override fun wrap(cursor: Cursor): PathInterpreter =
@ -431,8 +417,10 @@ class DataPathInterpreter(private val cursor: Cursor, private val volumeManager:
} }
} }
class VolumePathInterpreter(private val cursor: Cursor, private val volumeManager: VolumeManager) : private class VolumePathInterpreter(private val cursor: Cursor, volumeManager: VolumeManager) :
PathInterpreter { PathInterpreter {
private val displayNameIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
private val volumeIndex = private val volumeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
private val relativePathIndex = private val relativePathIndex =
@ -440,22 +428,29 @@ class VolumePathInterpreter(private val cursor: Cursor, private val volumeManage
private val volumes = volumeManager.getVolumes() private val volumes = volumeManager.getVolumes()
override fun populate(rawSong: RawSong) { override fun populate(rawSong: RawSong) {
// Find the StorageVolume whose MediaStore name corresponds to this song. // Find the StorageVolume whose MediaStore name corresponds to it.
// This is combined with the plain relative path column to create the directory.
val volumeName = cursor.getString(volumeIndex) val volumeName = cursor.getString(volumeIndex)
val relativePath = cursor.getString(relativePathIndex)
val volume = volumes.find { it.mediaStoreName == volumeName } val volume = volumes.find { it.mediaStoreName == volumeName }
// Relative path does not include file name, must use DISPLAY_NAME and add it
// in manually.
val relativePath = cursor.getString(relativePathIndex)
val displayName = cursor.getString(displayNameIndex)
val components = Components.parseUnix(relativePath).child(displayName)
if (volume != null) { if (volume != null) {
rawSong.directory = Path(volume, Components.parseUnix(relativePath)) rawSong.path = Path(volume, components)
} }
} }
class Factory(private val volumeManager: VolumeManager) : PathInterpreterFactory { class Factory(private val volumeManager: VolumeManager) : PathInterpreter.Factory {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
arrayOf( arrayOf(
// After API 29, we now have access to the volume name and relative // After API 29, we now have access to the volume name and relative
// path, which simplifies working with Paths significantly. // path, which hopefully are more standard and less likely to break
// compared to DATA.
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
MediaStore.Audio.AudioColumns.VOLUME_NAME, MediaStore.Audio.AudioColumns.VOLUME_NAME,
MediaStore.Audio.AudioColumns.RELATIVE_PATH) MediaStore.Audio.AudioColumns.RELATIVE_PATH)
@ -463,7 +458,7 @@ class VolumePathInterpreter(private val cursor: Cursor, private val volumeManage
// of the given directories, albeit with some conversion to the analogous MediaStore // of the given directories, albeit with some conversion to the analogous MediaStore
// column values. // column values.
override fun createSelector(paths: List<Path>): PathInterpreterFactory.Selector? { override fun createSelector(paths: List<Path>): PathInterpreter.Factory.Selector? {
val args = mutableListOf<String>() val args = mutableListOf<String>()
var template = "" var template = ""
for (i in paths.indices) { for (i in paths.indices) {
@ -488,7 +483,7 @@ class VolumePathInterpreter(private val cursor: Cursor, private val volumeManage
return null return null
} }
return PathInterpreterFactory.Selector(template, args) return PathInterpreter.Factory.Selector(template, args)
} }
override fun wrap(cursor: Cursor): PathInterpreter = override fun wrap(cursor: Cursor): PathInterpreter =
@ -496,9 +491,13 @@ class VolumePathInterpreter(private val cursor: Cursor, private val volumeManage
} }
} }
sealed interface TagInterpreter : Interpreter private sealed interface TagInterpreter : Interpreter {
interface Factory : Interpreter.Factory {
override fun wrap(cursor: Cursor): TagInterpreter
}
}
class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter { private class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter {
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
override fun populate(rawSong: RawSong) { override fun populate(rawSong: RawSong) {
@ -511,7 +510,7 @@ class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter {
} }
} }
class Factory : TagInterpreterFactory { class Factory : TagInterpreter.Factory {
override val projection: Array<String> override val projection: Array<String>
get() = arrayOf(MediaStore.Audio.AudioColumns.TRACK) get() = arrayOf(MediaStore.Audio.AudioColumns.TRACK)
@ -533,12 +532,13 @@ class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter {
* MediaStore's TRACK column, and combine the track and disc value into a single field where the * MediaStore's TRACK column, and combine the track and disc value into a single field where the
* disc number is the 4th+ digit. * disc number is the 4th+ digit.
* *
* @return The disc number extracted from the combined integer field, or null if the value was zero. * @return The disc number extracted from the combined integer field, or null if the value was
* zero.
*/ */
private fun Int.unpackDiscNo() = transformPositionField(div(1000), null) private fun Int.unpackDiscNo() = transformPositionField(div(1000), null)
} }
class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter { private class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter {
private val trackIndex = private val trackIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
private val discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) private val discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
@ -552,7 +552,7 @@ class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter {
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it } cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it }
} }
class Factory : TagInterpreterFactory { class Factory : TagInterpreter.Factory {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
arrayOf( arrayOf(