music: re-add escaped parsing

Re-add parsing by escaped separators.

Previously I removed it becaue the regex parsing was not being
cooperative. Turns out we really do have to write our own parser
code. Fun.
This commit is contained in:
Alexander Capehart 2022-09-25 15:00:48 -06:00
parent 66b9da0d5e
commit 94a74ebcf8
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 87 additions and 68 deletions

View file

@ -247,7 +247,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
*/ */
val path = val path =
Path( Path(
name = requireNotNull(raw.displayName) { "Invalid raw: No display name" }, name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" } parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" }
) )
@ -387,8 +387,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
var mediaStoreId: Long? = null, var mediaStoreId: Long? = null,
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
var name: String? = null, var name: String? = null,
var fileName: String? = null,
var sortName: String? = null, var sortName: String? = null,
var displayName: String? = null,
var directory: Directory? = null, var directory: Directory? = null,
var extensionMimeType: String? = null, var extensionMimeType: String? = null,
var formatMimeType: String? = null, var formatMimeType: String? = null,

View file

@ -314,7 +314,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name // Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
// from the android system. // from the android system.
raw.displayName = cursor.getStringOrNull(displayNameIndex) raw.fileName = cursor.getStringOrNull(displayNameIndex)
raw.durationMs = cursor.getLong(durationIndex) raw.durationMs = cursor.getLong(durationIndex)
raw.date = cursor.getIntOrNull(yearIndex)?.toDate() raw.date = cursor.getIntOrNull(yearIndex)?.toDate()
@ -411,8 +411,8 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
// that this only applies to below API 29, as beyond API 29, this field not being // that this only applies to below API 29, as beyond API 29, this field not being
// present would completely break the scoped storage system. Fill it in with DATA // present would completely break the scoped storage system. Fill it in with DATA
// if it's not available. // if it's not available.
if (raw.displayName == null) { if (raw.fileName == null) {
raw.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null } raw.fileName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
} }
// Find the volume that transforms the DATA field into a relative path. This is // Find the volume that transforms the DATA field into a relative path. This is

View file

@ -51,7 +51,39 @@ fun String.parseYear() = toIntOrNull()?.toDate()
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */ /** Parse an ISO-8601 time-stamp from this field into a [Date]. */
fun String.parseTimestamp() = Date.from(this) fun String.parseTimestamp() = Date.from(this)
private val SEPARATOR_REGEX_CACHE = mutableMapOf<String, Regex>() /**
* Parse a string by [selector], also handling string escaping.
*/
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
val split = mutableListOf<String>()
var currentString = ""
var i = 0
while (i < length) {
val a = get(i)
val b = getOrNull(i + 1)
if (selector(a)) {
split.add(currentString.trim())
currentString = ""
i++
continue
}
if (b != null && a == '\\' && selector(b)) {
currentString += b
i += 2
} else {
currentString += a
i++
}
}
if (currentString.isNotEmpty()) {
split.add(currentString.trim())
}
return split
}
/** /**
* Fully parse a multi-value tag. * Fully parse a multi-value tag.
@ -75,14 +107,7 @@ fun List<String>.parseMultiValue(settings: Settings) =
fun String.maybeParseSeparators(settings: Settings): List<String> { fun String.maybeParseSeparators(settings: Settings): List<String> {
// Get the separators the user desires. If null, we don't parse any. // Get the separators the user desires. If null, we don't parse any.
val separators = settings.separators ?: return listOf(this) val separators = settings.separators ?: return listOf(this)
return splitEscaped { separators.contains(it) }
// Try to cache compiled regexes for particular separator combinations.
val regex =
synchronized(SEPARATOR_REGEX_CACHE) {
SEPARATOR_REGEX_CACHE.getOrPut(separators) { Regex("[$separators]") }
}
return regex.split(this).map { it.trim() }
} }
/** Parse a multi-value tag into a [ReleaseType], handling separators in the process. */ /** Parse a multi-value tag into a [ReleaseType], handling separators in the process. */

View file

@ -39,8 +39,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
createTable(db, TABLE_NAME_STATE) createTable(db, TABLE_STATE)
createTable(db, TABLE_NAME_QUEUE) createTable(db, TABLE_QUEUE)
} }
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db)
@ -49,8 +49,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
private fun nuke(db: SQLiteDatabase) { private fun nuke(db: SQLiteDatabase) {
logD("Nuking database") logD("Nuking database")
db.apply { db.apply {
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE") execSQL("DROP TABLE IF EXISTS $TABLE_STATE")
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE") execSQL("DROP TABLE IF EXISTS $TABLE_QUEUE")
onCreate(this) onCreate(this)
} }
} }
@ -58,42 +58,36 @@ class PlaybackStateDatabase private constructor(context: Context) :
// --- DATABASE CONSTRUCTION FUNCTIONS --- // --- DATABASE CONSTRUCTION FUNCTIONS ---
/** Create a table for this database. */ /** Create a table for this database. */
private fun createTable(database: SQLiteDatabase, tableName: String) { private fun createTable(db: SQLiteDatabase, name: String) {
val command = StringBuilder() val command = StringBuilder()
command.append("CREATE TABLE IF NOT EXISTS $tableName(") command.append("CREATE TABLE IF NOT EXISTS $name(")
if (tableName == TABLE_NAME_STATE) { if (name == TABLE_STATE) {
constructStateTable(command) constructStateTable(command)
} else if (tableName == TABLE_NAME_QUEUE) { } else if (name == TABLE_QUEUE) {
constructQueueTable(command) constructQueueTable(command)
} }
database.execSQL(command.toString()) db.execSQL(command.toString())
} }
/** Construct a [StateColumns] table */ /** Construct a [StateColumns] table */
private fun constructStateTable(command: StringBuilder): StringBuilder { private fun constructStateTable(command: StringBuilder) =
command command
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,") .append("${StateColumns.ID} LONG PRIMARY KEY,")
.append("${StateColumns.COLUMN_SONG_UID} STRING,") .append("${StateColumns.SONG_UID} STRING,")
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,") .append("${StateColumns.POSITION} LONG NOT NULL,")
.append("${StateColumns.COLUMN_PARENT_UID} STRING,") .append("${StateColumns.PARENT_UID} STRING,")
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,") .append("${StateColumns.INDEX} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,") .append("${StateColumns.IS_SHUFFLED} BOOLEAN NOT NULL,")
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)") .append("${StateColumns.REPEAT_MODE} INTEGER NOT NULL)")
return command
}
/** Construct a [QueueColumns] table */ /** Construct a [QueueColumns] table */
private fun constructQueueTable(command: StringBuilder): StringBuilder { private fun constructQueueTable(command: StringBuilder) =
command command
.append("${QueueColumns.ID} LONG PRIMARY KEY,") .append("${QueueColumns.ID} LONG PRIMARY KEY,")
.append("${QueueColumns.SONG_UID} STRING NOT NULL)") .append("${QueueColumns.SONG_UID} STRING NOT NULL)")
return command
}
// --- INTERFACE FUNCTIONS --- // --- INTERFACE FUNCTIONS ---
fun read(library: MusicStore.Library): SavedState? { fun read(library: MusicStore.Library): SavedState? {
@ -121,18 +115,18 @@ class PlaybackStateDatabase private constructor(context: Context) :
} }
private fun readRawState(): RawState? { private fun readRawState(): RawState? {
return readableDatabase.queryAll(TABLE_NAME_STATE) { cursor -> return readableDatabase.queryAll(TABLE_STATE) { cursor ->
if (cursor.count == 0) { if (cursor.count == 0) {
return@queryAll null return@queryAll null
} }
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX) val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.INDEX)
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION) val posIndex = cursor.getColumnIndexOrThrow(StateColumns.POSITION)
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE) val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.REPEAT_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED) val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.IS_SHUFFLED)
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_UID) val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.SONG_UID)
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_UID) val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.PARENT_UID)
cursor.moveToFirst() cursor.moveToFirst()
@ -154,7 +148,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
val queue = mutableListOf<Song>() val queue = mutableListOf<Song>()
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor -> readableDatabase.queryAll(TABLE_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID) val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
@ -196,21 +190,21 @@ class PlaybackStateDatabase private constructor(context: Context) :
private fun writeRawState(rawState: RawState?) { private fun writeRawState(rawState: RawState?) {
writableDatabase.transaction { writableDatabase.transaction {
delete(TABLE_NAME_STATE, null, null) delete(TABLE_STATE, null, null)
if (rawState != null) { if (rawState != null) {
val stateData = val stateData =
ContentValues(10).apply { ContentValues(7).apply {
put(StateColumns.COLUMN_ID, 0) put(StateColumns.ID, 0)
put(StateColumns.COLUMN_SONG_UID, rawState.songUid.toString()) put(StateColumns.SONG_UID, rawState.songUid.toString())
put(StateColumns.COLUMN_POSITION, rawState.positionMs) put(StateColumns.POSITION, rawState.positionMs)
put(StateColumns.COLUMN_PARENT_UID, rawState.parentUid?.toString()) put(StateColumns.PARENT_UID, rawState.parentUid?.toString())
put(StateColumns.COLUMN_INDEX, rawState.index) put(StateColumns.INDEX, rawState.index)
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled) put(StateColumns.IS_SHUFFLED, rawState.isShuffled)
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode) put(StateColumns.REPEAT_MODE, rawState.repeatMode.intCode)
} }
insert(TABLE_NAME_STATE, null, stateData) insert(TABLE_STATE, null, stateData)
} }
} }
} }
@ -218,7 +212,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
/** Write a queue to the database. */ /** Write a queue to the database. */
private fun writeQueue(queue: List<Song>?) { private fun writeQueue(queue: List<Song>?) {
val database = writableDatabase val database = writableDatabase
database.transaction { delete(TABLE_NAME_QUEUE, null, null) } database.transaction { delete(TABLE_QUEUE, null, null) }
logD("Wiped queue db") logD("Wiped queue db")
@ -236,12 +230,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
i++ i++
val itemData = val itemData =
ContentValues(4).apply { ContentValues(2).apply {
put(QueueColumns.ID, idStart + i) put(QueueColumns.ID, idStart + i)
put(QueueColumns.SONG_UID, song.uid.toString()) put(QueueColumns.SONG_UID, song.uid.toString())
} }
insert(TABLE_NAME_QUEUE, null, itemData) insert(TABLE_QUEUE, null, itemData)
} }
} }
@ -273,13 +267,13 @@ class PlaybackStateDatabase private constructor(context: Context) :
) )
private object StateColumns { private object StateColumns {
const val COLUMN_ID = "id" const val ID = "id"
const val COLUMN_SONG_UID = "song_uid" const val SONG_UID = "song_uid"
const val COLUMN_POSITION = "position" const val POSITION = "position"
const val COLUMN_PARENT_UID = "parent" const val PARENT_UID = "parent"
const val COLUMN_INDEX = "queue_index" const val INDEX = "queue_index"
const val COLUMN_IS_SHUFFLED = "is_shuffling" const val IS_SHUFFLED = "is_shuffling"
const val COLUMN_REPEAT_MODE = "repeat_mode" const val REPEAT_MODE = "repeat_mode"
} }
private object QueueColumns { private object QueueColumns {
@ -291,8 +285,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
const val DB_NAME = "auxio_state_database.db" const val DB_NAME = "auxio_state_database.db"
const val DB_VERSION = 8 const val DB_VERSION = 8
const val TABLE_NAME_STATE = "playback_state_table" const val TABLE_STATE = "playback_state_table"
const val TABLE_NAME_QUEUE = "queue_table" const val TABLE_QUEUE = "queue_table"
@Volatile private var INSTANCE: PlaybackStateDatabase? = null @Volatile private var INSTANCE: PlaybackStateDatabase? = null