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:
parent
66b9da0d5e
commit
94a74ebcf8
4 changed files with 87 additions and 68 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue