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:
parent
9474e6f31a
commit
91d4b99aee
4 changed files with 178 additions and 96 deletions
|
|
@ -114,8 +114,6 @@ struct daemon_state {
|
||||||
json_t *script_settings_schema; /* array of {key,label,type,default} */
|
json_t *script_settings_schema; /* array of {key,label,type,default} */
|
||||||
json_t *script_settings; /* object: key -> value string (user-set)*/
|
json_t *script_settings; /* object: key -> value string (user-set)*/
|
||||||
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
|
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
|
||||||
char data_dir[PATH_MAX]; /* the resolved state dir (for blobs) */
|
|
||||||
pthread_mutex_t blob_lock; /* guards webui_<key>.json blob files */
|
|
||||||
pthread_mutex_t torrent_lock;
|
pthread_mutex_t torrent_lock;
|
||||||
torrent_task *torrents[MAX_TORRENTS];
|
torrent_task *torrents[MAX_TORRENTS];
|
||||||
size_t torrent_count;
|
size_t torrent_count;
|
||||||
|
|
@ -1313,70 +1311,6 @@ static json_t *rpc_set_script_settings(void *opaque, const json_t *params,
|
||||||
return script_status_json(state);
|
return script_status_json(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- generic web-UI blob store (RSS feeds, indexer config, ...) ------------ *
|
|
||||||
* The web layer owns these schemas; the daemon only persists them, one JSON
|
|
||||||
* document per key, under <state_dir>/webui_<key>.json. Keys are sanitized to a
|
|
||||||
* safe filename charset so a key can never escape the state directory. */
|
|
||||||
|
|
||||||
static bool blob_path(daemon_state *state, const char *key, char *out, size_t n) {
|
|
||||||
if (!key || !*key || !state->data_dir[0]) return false;
|
|
||||||
char safe[64];
|
|
||||||
size_t j = 0;
|
|
||||||
for (size_t i = 0; key[i] && j + 1 < sizeof safe; i++) {
|
|
||||||
char c = key[i];
|
|
||||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') || c == '_' || c == '-')
|
|
||||||
safe[j++] = c;
|
|
||||||
}
|
|
||||||
safe[j] = 0;
|
|
||||||
if (j == 0) return false;
|
|
||||||
return (size_t)snprintf(out, n, "%s/webui_%s.json", state->data_dir, safe) < n;
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_get_webui_blob(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
const char *key = json_string_value(json_object_get(params, "key"));
|
|
||||||
char path[PATH_MAX];
|
|
||||||
if (!blob_path(state, key, path, sizeof path)) {
|
|
||||||
*error = NAUT_ERR_INVAL;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
pthread_mutex_lock(&state->blob_lock);
|
|
||||||
json_error_t jerr;
|
|
||||||
json_t *value = json_load_file(path, 0, &jerr);
|
|
||||||
pthread_mutex_unlock(&state->blob_lock);
|
|
||||||
json_t *out = json_object();
|
|
||||||
if (!out) { json_decref(value); *error = NAUT_ERR_NOMEM; return NULL; }
|
|
||||||
json_object_set_new(out, "value", value ? value : json_null());
|
|
||||||
*error = NAUT_OK;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_set_webui_blob(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
const char *key = json_string_value(json_object_get(params, "key"));
|
|
||||||
json_t *value = json_object_get(params, "value");
|
|
||||||
char path[PATH_MAX];
|
|
||||||
if (!value || !blob_path(state, key, path, sizeof path)) {
|
|
||||||
*error = NAUT_ERR_INVAL;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
*error = NAUT_OK;
|
|
||||||
if (!state->persist_enabled) return json_object();
|
|
||||||
pthread_mutex_lock(&state->blob_lock);
|
|
||||||
char tmp[PATH_MAX + 8];
|
|
||||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
|
||||||
if (json_dump_file(value, tmp, JSON_INDENT(2)) != 0 ||
|
|
||||||
rename(tmp, path) != 0) {
|
|
||||||
NAUT_WARN("persist: write %s failed", path);
|
|
||||||
unlink(tmp);
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&state->blob_lock);
|
|
||||||
return json_object();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- data-overlap guard (block torrents that would write the same files) --- */
|
/* --- data-overlap guard (block torrents that would write the same files) --- */
|
||||||
|
|
||||||
static uint8_t *slurp_file(const char *path, size_t *len) {
|
static uint8_t *slurp_file(const char *path, size_t *len) {
|
||||||
|
|
@ -2303,8 +2237,6 @@ static bool register_commands(daemon_state *state) {
|
||||||
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "get_webui_blob", rpc_get_webui_blob, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "set_webui_blob", rpc_set_webui_blob, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
@ -2593,9 +2525,6 @@ static bool resolve_state_dir(daemon_state *state, const char *override) {
|
||||||
"%s/script_settings.json", dir) >=
|
"%s/script_settings.json", dir) >=
|
||||||
sizeof state->settings_file)
|
sizeof state->settings_file)
|
||||||
return false;
|
return false;
|
||||||
if ((size_t)snprintf(state->data_dir, sizeof state->data_dir, "%s", dir) >=
|
|
||||||
sizeof state->data_dir)
|
|
||||||
return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2701,7 +2630,6 @@ int main(int argc, char **argv) {
|
||||||
pthread_mutex_init(&state.script_lock, NULL);
|
pthread_mutex_init(&state.script_lock, NULL);
|
||||||
pthread_mutex_init(&state.settings_lock, NULL);
|
pthread_mutex_init(&state.settings_lock, NULL);
|
||||||
load_script_settings(&state); /* user-set values; schema comes from the script */
|
load_script_settings(&state); /* user-set values; schema comes from the script */
|
||||||
pthread_mutex_init(&state.blob_lock, NULL);
|
|
||||||
state.events = naut_event_bus_create();
|
state.events = naut_event_bus_create();
|
||||||
state.rpc = naut_rpc_registry_create();
|
state.rpc = naut_rpc_registry_create();
|
||||||
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
||||||
|
|
@ -2784,6 +2712,5 @@ int main(int argc, char **argv) {
|
||||||
json_decref(state.script_settings);
|
json_decref(state.script_settings);
|
||||||
json_decref(state.script_settings_schema);
|
json_decref(state.script_settings_schema);
|
||||||
pthread_mutex_destroy(&state.settings_lock);
|
pthread_mutex_destroy(&state.settings_lock);
|
||||||
pthread_mutex_destroy(&state.blob_lock);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1950,37 +1950,30 @@ static char *base64_encode(const unsigned char *in, size_t len) {
|
||||||
|
|
||||||
/* --- RSS persistence (via the daemon blob store) -------------------------- */
|
/* --- RSS persistence (via the daemon blob store) -------------------------- */
|
||||||
|
|
||||||
|
/* Persist feeds/rules/indexers to the web-UI's own database. */
|
||||||
static void rss_save(void) {
|
static void rss_save(void) {
|
||||||
|
if (!g_webui.store) return;
|
||||||
pthread_mutex_lock(&g_webui.rss_lock);
|
pthread_mutex_lock(&g_webui.rss_lock);
|
||||||
json_t *doc = json_pack("{s:O,s:O,s:O}",
|
json_t *feeds = json_deep_copy(g_webui.rss_feeds);
|
||||||
"feeds", g_webui.rss_feeds ? g_webui.rss_feeds : json_array(),
|
json_t *rules = json_deep_copy(g_webui.rss_rules);
|
||||||
"rules", g_webui.rss_rules ? g_webui.rss_rules : json_array(),
|
json_t *idx = json_deep_copy(g_webui.indexers);
|
||||||
"indexers", g_webui.indexers ? g_webui.indexers : json_array());
|
|
||||||
pthread_mutex_unlock(&g_webui.rss_lock);
|
pthread_mutex_unlock(&g_webui.rss_lock);
|
||||||
if (!doc) return;
|
if (feeds) { webui_store_save_feeds(g_webui.store, feeds); json_decref(feeds); }
|
||||||
json_t *params = json_pack("{s:s,s:o}", "key", "rss", "value", doc);
|
if (rules) { webui_store_save_rules(g_webui.store, rules); json_decref(rules); }
|
||||||
if (!params) { json_decref(doc); return; }
|
if (idx) { webui_store_save_indexers(g_webui.store, idx); json_decref(idx); }
|
||||||
json_t *reply = rpc_call_json("set_webui_blob", params);
|
|
||||||
json_decref(params);
|
|
||||||
if (reply) json_decref(reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void rss_load(void) {
|
static void rss_load(void) {
|
||||||
json_t *params = json_pack("{s:s}", "key", "rss");
|
if (!g_webui.store) return;
|
||||||
json_t *reply = rpc_call_json("get_webui_blob", params);
|
json_t *feeds = json_array(), *rules = json_array(), *idx = json_array();
|
||||||
json_decref(params);
|
bool of = webui_store_load_feeds(g_webui.store, feeds);
|
||||||
json_t *value = reply ? json_object_get(reply, "value") : NULL;
|
bool orr = webui_store_load_rules(g_webui.store, rules);
|
||||||
|
bool oi = webui_store_load_indexers(g_webui.store, idx);
|
||||||
pthread_mutex_lock(&g_webui.rss_lock);
|
pthread_mutex_lock(&g_webui.rss_lock);
|
||||||
if (json_is_object(value)) {
|
if (of) { json_decref(g_webui.rss_feeds); g_webui.rss_feeds = feeds; } else json_decref(feeds);
|
||||||
json_t *feeds = json_object_get(value, "feeds");
|
if (orr) { json_decref(g_webui.rss_rules); g_webui.rss_rules = rules; } else json_decref(rules);
|
||||||
json_t *rules = json_object_get(value, "rules");
|
if (oi) { json_decref(g_webui.indexers); g_webui.indexers = idx; } else json_decref(idx);
|
||||||
json_t *idx = json_object_get(value, "indexers");
|
|
||||||
if (json_is_array(feeds)) { json_decref(g_webui.rss_feeds); g_webui.rss_feeds = json_deep_copy(feeds); }
|
|
||||||
if (json_is_array(rules)) { json_decref(g_webui.rss_rules); g_webui.rss_rules = json_deep_copy(rules); }
|
|
||||||
if (json_is_array(idx)) { json_decref(g_webui.indexers); g_webui.indexers = json_deep_copy(idx); }
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&g_webui.rss_lock);
|
pthread_mutex_unlock(&g_webui.rss_lock);
|
||||||
if (reply) json_decref(reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- auto-download: hand a matched article to the daemon ------------------ */
|
/* --- auto-download: hand a matched article to the daemon ------------------ */
|
||||||
|
|
|
||||||
|
|
@ -388,3 +388,155 @@ bool webui_store_load_tags(webui_store *s, json_t *out) {
|
||||||
pthread_mutex_unlock(&s->lock);
|
pthread_mutex_unlock(&s->lock);
|
||||||
return ok;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,14 @@ bool webui_store_load_categories(webui_store *s, json_t *out);
|
||||||
bool webui_store_save_tags(webui_store *s, json_t *tags);
|
bool webui_store_save_tags(webui_store *s, json_t *tags);
|
||||||
bool webui_store_load_tags(webui_store *s, json_t *out);
|
bool webui_store_load_tags(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
/* --- RSS: feeds, auto-download rules, Torznab indexers (owned here) -------- *
|
||||||
|
* save_* replace the whole list atomically; load_* append to the array `out`,
|
||||||
|
* rebuilding the exact JSON shapes the web layer/UI use. */
|
||||||
|
bool webui_store_save_feeds(webui_store *s, json_t *feeds);
|
||||||
|
bool webui_store_load_feeds(webui_store *s, json_t *out);
|
||||||
|
bool webui_store_save_rules(webui_store *s, json_t *rules);
|
||||||
|
bool webui_store_load_rules(webui_store *s, json_t *out);
|
||||||
|
bool webui_store_save_indexers(webui_store *s, json_t *indexers);
|
||||||
|
bool webui_store_load_indexers(webui_store *s, json_t *out);
|
||||||
|
|
||||||
#endif /* NAUT_WEBUI_STORE_H */
|
#endif /* NAUT_WEBUI_STORE_H */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue