webui: store RSS feeds/rules/indexers in the webui DB

Move RSS persistence out of the daemon blob store and into the webui's
own SQLite DB (feeds, rules, indexers tables; articles + affectedFeeds
held as JSON columns). Remove the now-unused daemon blob store
(set/get_webui_blob, blob_lock, data_dir).

With this, all webui-owned state — accounts, taxonomy, RSS — lives in
the webui DB; the daemon only keeps naut's own data (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:
ookami125 2026-06-23 22:11:17 -04:00
parent 9474e6f31a
commit 91d4b99aee
4 changed files with 178 additions and 96 deletions

View file

@ -388,3 +388,155 @@ bool webui_store_load_tags(webui_store *s, json_t *out) {
pthread_mutex_unlock(&s->lock);
return ok;
}
/* --- RSS ------------------------------------------------------------------ */
static int int_of(json_t *o, const char *k) {
return json_boolean_value(json_object_get(o, k)) ? 1 : 0;
}
/* Bind a json array column as a compact JSON string (SQLite copies it). */
static void bind_json_array(sqlite3_stmt *st, int col, json_t *arr) {
char *s = json_dumps(json_is_array(arr) ? arr : json_array(), JSON_COMPACT);
sqlite3_bind_text(st, col, s ? s : "[]", -1, SQLITE_TRANSIENT);
free(s);
}
/* Parse a TEXT column holding a JSON array; returns a new array (never NULL). */
static json_t *array_col(sqlite3_stmt *st, int col) {
const char *txt = (const char *)sqlite3_column_text(st, col);
if (txt) {
json_t *a = json_loads(txt, 0, NULL);
if (json_is_array(a)) return a;
json_decref(a);
}
return json_array();
}
static void bind_feed(sqlite3_stmt *st, json_t *f) {
sqlite3_bind_text(st, 1, str_or(f, "name", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, str_or(f, "url", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 3, (sqlite3_int64)json_integer_value(json_object_get(f, "lastUpdate")));
bind_json_array(st, 4, json_object_get(f, "articles"));
}
bool webui_store_save_feeds(webui_store *s, json_t *feeds) {
return replace_table(s, "DELETE FROM feeds;",
"INSERT OR REPLACE INTO feeds (name, url, last_update, articles) VALUES (?,?,?,?);",
feeds, bind_feed);
}
bool webui_store_load_feeds(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, url, last_update, articles FROM feeds 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 *u = (const char *)sqlite3_column_text(st, 1);
json_array_append_new(out, json_pack("{s:s,s:s,s:I,s:o}",
"name", n ? n : "", "url", u ? u : "",
"lastUpdate", (json_int_t)sqlite3_column_int64(st, 2),
"articles", array_col(st, 3)));
}
}
sqlite3_finalize(st);
pthread_mutex_unlock(&s->lock);
return ok;
}
static void bind_rule(sqlite3_stmt *st, json_t *r) {
sqlite3_bind_text(st, 1, str_or(r, "name", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(st, 2, int_of(r, "enabled"));
sqlite3_bind_int(st, 3, int_of(r, "useRegex"));
sqlite3_bind_int(st, 4, int_of(r, "addPaused"));
sqlite3_bind_text(st, 5, str_or(r, "mustContain", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 6, str_or(r, "mustNotContain", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 7, str_or(r, "assignedCategory", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 8, str_or(r, "savePath", ""), -1, SQLITE_TRANSIENT);
bind_json_array(st, 9, json_object_get(r, "affectedFeeds"));
sqlite3_bind_int64(st, 10, (sqlite3_int64)json_integer_value(json_object_get(r, "lastMatch")));
}
bool webui_store_save_rules(webui_store *s, json_t *rules) {
return replace_table(s, "DELETE FROM rules;",
"INSERT OR REPLACE INTO rules (name, enabled, use_regex, add_paused,"
" must_contain, must_not_contain, assigned_category, save_path,"
" affected_feeds, last_match) VALUES (?,?,?,?,?,?,?,?,?,?);",
rules, bind_rule);
}
bool webui_store_load_rules(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, enabled, use_regex, add_paused, must_contain,"
" must_not_contain, assigned_category, save_path, affected_feeds,"
" last_match FROM rules 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 *mc = (const char *)sqlite3_column_text(st, 4);
const char *mn = (const char *)sqlite3_column_text(st, 5);
const char *ac = (const char *)sqlite3_column_text(st, 6);
const char *sp = (const char *)sqlite3_column_text(st, 7);
json_array_append_new(out, json_pack(
"{s:s,s:b,s:b,s:b,s:s,s:s,s:s,s:s,s:o,s:I}",
"name", n ? n : "",
"enabled", sqlite3_column_int(st, 1),
"useRegex", sqlite3_column_int(st, 2),
"addPaused", sqlite3_column_int(st, 3),
"mustContain", mc ? mc : "",
"mustNotContain", mn ? mn : "",
"assignedCategory", ac ? ac : "",
"savePath", sp ? sp : "",
"affectedFeeds", array_col(st, 8),
"lastMatch", (json_int_t)sqlite3_column_int64(st, 9)));
}
}
sqlite3_finalize(st);
pthread_mutex_unlock(&s->lock);
return ok;
}
static void bind_indexer(sqlite3_stmt *st, json_t *x) {
sqlite3_bind_text(st, 1, str_or(x, "name", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, str_or(x, "url", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, str_or(x, "apikey", ""), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(st, 4, int_of(x, "enabled"));
}
bool webui_store_save_indexers(webui_store *s, json_t *indexers) {
return replace_table(s, "DELETE FROM indexers;",
"INSERT OR REPLACE INTO indexers (name, url, apikey, enabled) VALUES (?,?,?,?);",
indexers, bind_indexer);
}
bool webui_store_load_indexers(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, url, apikey, enabled FROM indexers 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 *u = (const char *)sqlite3_column_text(st, 1);
const char *k = (const char *)sqlite3_column_text(st, 2);
json_array_append_new(out, json_pack("{s:s,s:s,s:s,s:b}",
"name", n ? n : "", "url", u ? u : "", "apikey", k ? k : "",
"enabled", sqlite3_column_int(st, 3)));
}
}
sqlite3_finalize(st);
pthread_mutex_unlock(&s->lock);
return ok;
}