webui: store category/tag taxonomy in the webui DB
Move the category + tag lists out of the daemon (labels.json + the get/set_label_taxonomy RPCs) and into the webui's own SQLite DB (categories, tags tables). The daemon no longer persists webui taxonomy; per-torrent labels still flow through set_labels for Lua. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1b79fe8079
commit
9474e6f31a
4 changed files with 160 additions and 146 deletions
|
|
@ -73,7 +73,32 @@ webui_store *webui_store_open(const char *path) {
|
|||
" pw_salt TEXT NOT NULL,"
|
||||
" pw_iters INTEGER NOT NULL,"
|
||||
" role TEXT NOT NULL DEFAULT 'user',"
|
||||
" created_at INTEGER NOT NULL);";
|
||||
" created_at INTEGER NOT NULL);"
|
||||
"CREATE TABLE IF NOT EXISTS categories ("
|
||||
" name TEXT PRIMARY KEY,"
|
||||
" save_path TEXT NOT NULL DEFAULT '');"
|
||||
"CREATE TABLE IF NOT EXISTS tags (name TEXT PRIMARY KEY);"
|
||||
"CREATE TABLE IF NOT EXISTS feeds ("
|
||||
" name TEXT PRIMARY KEY,"
|
||||
" url TEXT NOT NULL,"
|
||||
" last_update INTEGER NOT NULL DEFAULT 0,"
|
||||
" articles TEXT NOT NULL DEFAULT '[]');"
|
||||
"CREATE TABLE IF NOT EXISTS rules ("
|
||||
" name TEXT PRIMARY KEY,"
|
||||
" enabled INTEGER NOT NULL DEFAULT 1,"
|
||||
" use_regex INTEGER NOT NULL DEFAULT 0,"
|
||||
" add_paused INTEGER NOT NULL DEFAULT 0,"
|
||||
" must_contain TEXT NOT NULL DEFAULT '',"
|
||||
" must_not_contain TEXT NOT NULL DEFAULT '',"
|
||||
" assigned_category TEXT NOT NULL DEFAULT '',"
|
||||
" save_path TEXT NOT NULL DEFAULT '',"
|
||||
" affected_feeds TEXT NOT NULL DEFAULT '[]',"
|
||||
" last_match INTEGER NOT NULL DEFAULT 0);"
|
||||
"CREATE TABLE IF NOT EXISTS indexers ("
|
||||
" name TEXT PRIMARY KEY,"
|
||||
" url TEXT NOT NULL DEFAULT '',"
|
||||
" apikey TEXT NOT NULL DEFAULT '',"
|
||||
" enabled INTEGER NOT NULL DEFAULT 1);";
|
||||
char *err = NULL;
|
||||
if (sqlite3_exec(s->db, schema, NULL, NULL, &err) != SQLITE_OK) {
|
||||
sqlite3_free(err);
|
||||
|
|
@ -272,3 +297,94 @@ bool webui_store_list_users(webui_store *s, json_t *out) {
|
|||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* --- taxonomy ------------------------------------------------------------- */
|
||||
|
||||
/* Replace one table's contents from a json array, inside a transaction. The
|
||||
* `bind` callback binds each element's columns onto the prepared INSERT. */
|
||||
static bool replace_table(webui_store *s, const char *del_sql,
|
||||
const char *ins_sql, json_t *items,
|
||||
void (*bind)(sqlite3_stmt *, json_t *)) {
|
||||
if (!s || !json_is_array(items)) return false;
|
||||
pthread_mutex_lock(&s->lock);
|
||||
bool ok = sqlite3_exec(s->db, "BEGIN;", NULL, NULL, NULL) == SQLITE_OK &&
|
||||
sqlite3_exec(s->db, del_sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (ok && sqlite3_prepare_v2(s->db, ins_sql, -1, &st, NULL) == SQLITE_OK) {
|
||||
size_t i; json_t *v;
|
||||
json_array_foreach(items, i, v) {
|
||||
bind(st, v);
|
||||
if (sqlite3_step(st) != SQLITE_DONE) { ok = false; break; }
|
||||
sqlite3_reset(st);
|
||||
}
|
||||
} else ok = false;
|
||||
sqlite3_finalize(st);
|
||||
sqlite3_exec(s->db, ok ? "COMMIT;" : "ROLLBACK;", NULL, NULL, NULL);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static const char *str_or(json_t *o, const char *k, const char *fallback) {
|
||||
const char *v = json_string_value(json_object_get(o, k));
|
||||
return v ? v : fallback;
|
||||
}
|
||||
|
||||
static void bind_category(sqlite3_stmt *st, json_t *c) {
|
||||
sqlite3_bind_text(st, 1, str_or(c, "name", ""), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(st, 2, str_or(c, "savePath", ""), -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
bool webui_store_save_categories(webui_store *s, json_t *cats) {
|
||||
return replace_table(s, "DELETE FROM categories;",
|
||||
"INSERT OR REPLACE INTO categories (name, save_path) VALUES (?,?);",
|
||||
cats, bind_category);
|
||||
}
|
||||
|
||||
bool webui_store_load_categories(webui_store *s, json_t *out) {
|
||||
if (!s || !json_is_array(out)) return false;
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db,
|
||||
"SELECT name, save_path FROM categories ORDER BY name;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
ok = true;
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *n = (const char *)sqlite3_column_text(st, 0);
|
||||
const char *p = (const char *)sqlite3_column_text(st, 1);
|
||||
json_array_append_new(out, json_pack("{s:s,s:s}",
|
||||
"name", n ? n : "", "savePath", p ? p : ""));
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void bind_tag(sqlite3_stmt *st, json_t *t) {
|
||||
sqlite3_bind_text(st, 1, json_string_value(t) ? json_string_value(t) : "",
|
||||
-1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
bool webui_store_save_tags(webui_store *s, json_t *tags) {
|
||||
return replace_table(s, "DELETE FROM tags;",
|
||||
"INSERT OR REPLACE INTO tags (name) VALUES (?);", tags, bind_tag);
|
||||
}
|
||||
|
||||
bool webui_store_load_tags(webui_store *s, json_t *out) {
|
||||
if (!s || !json_is_array(out)) return false;
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db, "SELECT name FROM tags ORDER BY name;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
ok = true;
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *n = (const char *)sqlite3_column_text(st, 0);
|
||||
json_array_append_new(out, json_string(n ? n : ""));
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue