Skip to content

Commit

Permalink
music: refine new mediastoreextractor impl
Browse files Browse the repository at this point in the history
- Make the interpreters use a more conventional naming structure
- Remove the redundant file name extraction that is largely an artifact
of older versions
  • Loading branch information
OxygenCobalt committed Jan 1, 2024
1 parent 6b9f686 commit ed519ee
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,37 +74,31 @@ class SongImpl(
}
override val name =
nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.fileName}: No title" },
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" },
rawSong.sortName)

override val track = rawSong.track
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
override val date = rawSong.date
override val uri =
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" }
.toAudioUri()
override val path =
requireNotNull(rawSong.directory) { "Invalid raw ${rawSong.fileName}: No parent directory" }
.file(
requireNotNull(rawSong.fileName) {
"Invalid raw ${rawSong.fileName}: No display name"
})
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri()
override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" }
override val mimeType =
MimeType(
fromExtension =
requireNotNull(rawSong.extensionMimeType) {
"Invalid raw ${rawSong.fileName}: No mime type"
"Invalid raw ${rawSong.path}: No mime type"
},
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 =
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.fileName}: No duration" }
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" }
override val replayGainAdjustment =
ReplayGainAdjustment(
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)

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
override val album: Album
Expand Down Expand Up @@ -170,12 +164,12 @@ class SongImpl(
RawAlbum(
mediaStoreId =
requireNotNull(rawSong.albumMediaStoreId) {
"Invalid raw ${rawSong.fileName}: No album id"
"Invalid raw ${rawSong.path}: No album id"
},
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
name =
requireNotNull(rawSong.albumName) {
"Invalid raw ${rawSong.fileName}: No album name"
"Invalid raw ${rawSong.path}: No album name"
},
sortName = rawSong.albumSortName,
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ data class RawSong(
/** The latest date the [SongImpl]'s audio file was modified, as a unix epoch timestamp. */
var dateModified: Long? = null,
/** @see Song.path */
var fileName: String? = null,
/** @see Song.path */
var directory: Path? = null,
var path: Path? = null,
/** @see Song.size */
var size: Long? = null,
/** @see Song.durationMs */
Expand Down
114 changes: 57 additions & 57 deletions app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import android.os.Build
import android.provider.MediaStore
import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull
import java.io.File
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.Cache
Expand Down Expand Up @@ -119,8 +118,8 @@ interface MediaStoreExtractor {

private class MediaStoreExtractorImpl(
private val context: Context,
private val pathInterpreterFactory: PathInterpreterFactory,
private val tagInterpreterFactory: TagInterpreterFactory
private val pathInterpreterFactory: PathInterpreter.Factory,
private val tagInterpreterFactory: TagInterpreter.Factory
) : MediaStoreExtractor {
override suspend fun query(
constraints: MediaStoreExtractor.Constraints
Expand Down Expand Up @@ -239,8 +238,6 @@ private class MediaStoreExtractorImpl(
) : MediaStoreExtractor.Query {
private val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
private val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
private val displayNameIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
private val mimeTypeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
private val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
Expand All @@ -267,9 +264,6 @@ private class MediaStoreExtractorImpl(
rawSong.mediaStoreId = cursor.getLong(idIndex)
rawSong.dateAdded = cursor.getLong(dateAddedIndex)
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.albumMediaStoreId = cursor.getLong(albumIdIndex)
pathInterpreter.populate(rawSong)
Expand All @@ -289,7 +283,8 @@ private class MediaStoreExtractorImpl(
// 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
// 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)
// 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
Expand Down Expand Up @@ -336,7 +331,6 @@ private class MediaStoreExtractorImpl(
MediaStore.Audio.AudioColumns._ID,
MediaStore.Audio.AudioColumns.DATE_ADDED,
MediaStore.Audio.AudioColumns.DATE_MODIFIED,
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
MediaStore.Audio.AudioColumns.SIZE,
MediaStore.Audio.AudioColumns.DURATION,
MediaStore.Audio.AudioColumns.MIME_TYPE,
Expand All @@ -349,62 +343,54 @@ private class MediaStoreExtractorImpl(
}
}

interface Interpreter {
private interface Interpreter {
fun populate(rawSong: RawSong)
}

interface InterpreterFactory {
val projection: Array<String>
interface Factory {
val projection: Array<String>

fun wrap(cursor: Cursor): Interpreter
fun wrap(cursor: Cursor): Interpreter
}
}

interface PathInterpreterFactory : InterpreterFactory {
override fun wrap(cursor: Cursor): PathInterpreter
private sealed interface PathInterpreter : Interpreter {
interface Factory : Interpreter.Factory {
override fun wrap(cursor: Cursor): PathInterpreter

fun createSelector(paths: List<Path>): Selector?
fun createSelector(paths: List<Path>): Selector?

data class Selector(val template: String, val args: List<String>)
}

interface TagInterpreterFactory : InterpreterFactory {
override fun wrap(cursor: Cursor): TagInterpreter
data class Selector(val template: String, val args: List<String>)
}
}

sealed interface PathInterpreter : Interpreter

class DataPathInterpreter(private val cursor: Cursor, private val volumeManager: VolumeManager) :
PathInterpreter {
private class DataPathInterpreter(
private val cursor: Cursor,
private val volumeManager: VolumeManager
) : PathInterpreter {
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
private val volumes = volumeManager.getVolumes()

override fun populate(rawSong: RawSong) {
val data = 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 }
}
val data = Components.parseUnix(cursor.getString(dataIndex))

// Find the volume that transforms the DATA column into a relative path. This is
// the Directory we will use.
val rawPath = Components.parseUnix(data)
for (volume in volumes) {
val volumePath = volume.components ?: continue
if (volumePath.contains(rawPath)) {
rawSong.directory = Path(volume, rawPath.depth(volumePath.components.size))
if (volumePath.contains(data)) {
rawSong.path = Path(volume, data.depth(volumePath.components.size))
break
}
}
}

class Factory(private val volumeManager: VolumeManager) : PathInterpreterFactory {
class Factory(private val volumeManager: VolumeManager) : PathInterpreter.Factory {
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>()
var template = ""
for (i in paths.indices) {
Expand All @@ -423,47 +409,56 @@ class DataPathInterpreter(private val cursor: Cursor, private val volumeManager:
return null
}

return PathInterpreterFactory.Selector(template, args)
return PathInterpreter.Factory.Selector(template, args)
}

override fun wrap(cursor: Cursor): PathInterpreter =
DataPathInterpreter(cursor, volumeManager)
}
}

class VolumePathInterpreter(private val cursor: Cursor, private val volumeManager: VolumeManager) :
private class VolumePathInterpreter(private val cursor: Cursor, volumeManager: VolumeManager) :
PathInterpreter {
private val displayNameIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
private val volumeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
private val relativePathIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
private val volumes = volumeManager.getVolumes()

override fun populate(rawSong: RawSong) {
// Find the StorageVolume whose MediaStore name corresponds to this song.
// This is combined with the plain relative path column to create the directory.
// Find the StorageVolume whose MediaStore name corresponds to it.
val volumeName = cursor.getString(volumeIndex)
val relativePath = cursor.getString(relativePathIndex)
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) {
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>
get() =
arrayOf(
// 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.RELATIVE_PATH)

// The selector should be configured to compare both the volume name and relative path
// of the given directories, albeit with some conversion to the analogous MediaStore
// column values.

override fun createSelector(paths: List<Path>): PathInterpreterFactory.Selector? {
override fun createSelector(paths: List<Path>): PathInterpreter.Factory.Selector? {
val args = mutableListOf<String>()
var template = ""
for (i in paths.indices) {
Expand All @@ -488,17 +483,21 @@ class VolumePathInterpreter(private val cursor: Cursor, private val volumeManage
return null
}

return PathInterpreterFactory.Selector(template, args)
return PathInterpreter.Factory.Selector(template, args)
}

override fun wrap(cursor: Cursor): PathInterpreter =
VolumePathInterpreter(cursor, volumeManager)
}
}

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)

override fun populate(rawSong: RawSong) {
Expand All @@ -511,7 +510,7 @@ class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter {
}
}

class Factory : TagInterpreterFactory {
class Factory : TagInterpreter.Factory {
override val projection: Array<String>
get() = arrayOf(MediaStore.Audio.AudioColumns.TRACK)

Expand All @@ -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
* 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)
}

class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter {
private class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter {
private val trackIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
private val discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
Expand All @@ -552,7 +552,7 @@ class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter {
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it }
}

class Factory : TagInterpreterFactory {
class Factory : TagInterpreter.Factory {
override val projection: Array<String>
get() =
arrayOf(
Expand Down

0 comments on commit ed519ee

Please sign in to comment.