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 =
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" }
)
@ -387,8 +387,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
var mediaStoreId: Long? = null,
var musicBrainzId: String? = null,
var name: String? = null,
var fileName: String? = null,
var sortName: String? = null,
var displayName: String? = null,
var directory: Directory? = null,
var extensionMimeType: 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
// from the android system.
raw.displayName = cursor.getStringOrNull(displayNameIndex)
raw.fileName = cursor.getStringOrNull(displayNameIndex)
raw.durationMs = cursor.getLong(durationIndex)
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
// present would completely break the scoped storage system. Fill it in with DATA
// if it's not available.
if (raw.displayName == null) {
raw.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
if (raw.fileName == null) {
raw.fileName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
}
// 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]. */
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.
@ -75,14 +107,7 @@ fun List<String>.parseMultiValue(settings: Settings) =
fun String.maybeParseSeparators(settings: Settings): List<String> {
// Get the separators the user desires. If null, we don't parse any.
val separators = settings.separators ?: return listOf(this)
// 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() }
return splitEscaped { separators.contains(it) }
}
/** 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) {
override fun onCreate(db: SQLiteDatabase) {
createTable(db, TABLE_NAME_STATE)
createTable(db, TABLE_NAME_QUEUE)
createTable(db, TABLE_STATE)
createTable(db, TABLE_QUEUE)
}
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) {
logD("Nuking database")
db.apply {
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
execSQL("DROP TABLE IF EXISTS $TABLE_STATE")
execSQL("DROP TABLE IF EXISTS $TABLE_QUEUE")
onCreate(this)
}
}
@ -58,42 +58,36 @@ class PlaybackStateDatabase private constructor(context: Context) :
// --- DATABASE CONSTRUCTION FUNCTIONS ---
/** Create a table for this database. */
private fun createTable(database: SQLiteDatabase, tableName: String) {
private fun createTable(db: SQLiteDatabase, name: String) {
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)
} else if (tableName == TABLE_NAME_QUEUE) {
} else if (name == TABLE_QUEUE) {
constructQueueTable(command)
}
database.execSQL(command.toString())
db.execSQL(command.toString())
}
/** Construct a [StateColumns] table */
private fun constructStateTable(command: StringBuilder): StringBuilder {
private fun constructStateTable(command: StringBuilder) =
command
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
.append("${StateColumns.COLUMN_SONG_UID} STRING,")
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
.append("${StateColumns.COLUMN_PARENT_UID} STRING,")
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
return command
}
.append("${StateColumns.ID} LONG PRIMARY KEY,")
.append("${StateColumns.SONG_UID} STRING,")
.append("${StateColumns.POSITION} LONG NOT NULL,")
.append("${StateColumns.PARENT_UID} STRING,")
.append("${StateColumns.INDEX} INTEGER NOT NULL,")
.append("${StateColumns.IS_SHUFFLED} BOOLEAN NOT NULL,")
.append("${StateColumns.REPEAT_MODE} INTEGER NOT NULL)")
/** Construct a [QueueColumns] table */
private fun constructQueueTable(command: StringBuilder): StringBuilder {
private fun constructQueueTable(command: StringBuilder) =
command
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
.append("${QueueColumns.SONG_UID} STRING NOT NULL)")
return command
}
// --- INTERFACE FUNCTIONS ---
fun read(library: MusicStore.Library): SavedState? {
@ -121,18 +115,18 @@ class PlaybackStateDatabase private constructor(context: Context) :
}
private fun readRawState(): RawState? {
return readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
return readableDatabase.queryAll(TABLE_STATE) { cursor ->
if (cursor.count == 0) {
return@queryAll null
}
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.INDEX)
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_UID)
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_UID)
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.POSITION)
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.REPEAT_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.IS_SHUFFLED)
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.SONG_UID)
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.PARENT_UID)
cursor.moveToFirst()
@ -154,7 +148,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
val queue = mutableListOf<Song>()
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
readableDatabase.queryAll(TABLE_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
while (cursor.moveToNext()) {
@ -196,21 +190,21 @@ class PlaybackStateDatabase private constructor(context: Context) :
private fun writeRawState(rawState: RawState?) {
writableDatabase.transaction {
delete(TABLE_NAME_STATE, null, null)
delete(TABLE_STATE, null, null)
if (rawState != null) {
val stateData =
ContentValues(10).apply {
put(StateColumns.COLUMN_ID, 0)
put(StateColumns.COLUMN_SONG_UID, rawState.songUid.toString())
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
put(StateColumns.COLUMN_PARENT_UID, rawState.parentUid?.toString())
put(StateColumns.COLUMN_INDEX, rawState.index)
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
ContentValues(7).apply {
put(StateColumns.ID, 0)
put(StateColumns.SONG_UID, rawState.songUid.toString())
put(StateColumns.POSITION, rawState.positionMs)
put(StateColumns.PARENT_UID, rawState.parentUid?.toString())
put(StateColumns.INDEX, rawState.index)
put(StateColumns.IS_SHUFFLED, rawState.isShuffled)
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. */
private fun writeQueue(queue: List<Song>?) {
val database = writableDatabase
database.transaction { delete(TABLE_NAME_QUEUE, null, null) }
database.transaction { delete(TABLE_QUEUE, null, null) }
logD("Wiped queue db")
@ -236,12 +230,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
i++
val itemData =
ContentValues(4).apply {
ContentValues(2).apply {
put(QueueColumns.ID, idStart + i)
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 {
const val COLUMN_ID = "id"
const val COLUMN_SONG_UID = "song_uid"
const val COLUMN_POSITION = "position"
const val COLUMN_PARENT_UID = "parent"
const val COLUMN_INDEX = "queue_index"
const val COLUMN_IS_SHUFFLED = "is_shuffling"
const val COLUMN_REPEAT_MODE = "repeat_mode"
const val ID = "id"
const val SONG_UID = "song_uid"
const val POSITION = "position"
const val PARENT_UID = "parent"
const val INDEX = "queue_index"
const val IS_SHUFFLED = "is_shuffling"
const val REPEAT_MODE = "repeat_mode"
}
private object QueueColumns {
@ -291,8 +285,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
const val DB_NAME = "auxio_state_database.db"
const val DB_VERSION = 8
const val TABLE_NAME_STATE = "playback_state_table"
const val TABLE_NAME_QUEUE = "queue_table"
const val TABLE_STATE = "playback_state_table"
const val TABLE_QUEUE = "queue_table"
@Volatile private var INSTANCE: PlaybackStateDatabase? = null