webui: RSS auto-download + Torznab search backend
Add a real backend behind the RSS and Search tabs: - nautd gains a generic web-UI blob store (set/get_webui_blob) so the web layer can persist RSS feeds, auto-download rules and indexer config under <state_dir>/webui_<key>.json. - The webui plugin runs a background poller that fetches each feed over HTTP(S), parses RSS 2.0 / Atom items (title, link, enclosure, size, pubDate, magnet incl. torrent:magnetURI), dedupes, and stores articles. - Auto-download rules (substring or POSIX regex, mustContain/ mustNotContain, per-feed scope) fire on newly-seen items and add the torrent via the daemon — from a magnet, or by fetching a .torrent enclosure and uploading its bytes — applying category/save path/paused. - Search queries every enabled Torznab indexer and merges results (name, size, seeders, leechers, magnet/.torrent), exposed as searchPlugins in /api/meta. New endpoints: GET/POST /api/rss(+/delete), /api/rss/rules(+/delete), /api/indexers(+/delete), GET /api/search?q=. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0393ed429b
commit
d6e0e51175
2 changed files with 802 additions and 9 deletions
|
|
@ -121,6 +121,8 @@ struct daemon_state {
|
|||
json_t *label_categories; /* array of {name, savePath} */
|
||||
json_t *label_tags; /* array of tag name strings */
|
||||
char taxonomy_file[PATH_MAX]; /* <state_dir>/labels.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;
|
||||
torrent_task *torrents[MAX_TORRENTS];
|
||||
size_t torrent_count;
|
||||
|
|
@ -1401,6 +1403,70 @@ static json_t *rpc_set_label_taxonomy(void *opaque, const json_t *params,
|
|||
return json_object();
|
||||
}
|
||||
|
||||
/* --- 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) --- */
|
||||
|
||||
static uint8_t *slurp_file(const char *path, size_t *len) {
|
||||
|
|
@ -2329,6 +2395,8 @@ static bool register_commands(daemon_state *state) {
|
|||
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "get_label_taxonomy", rpc_get_label_taxonomy, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "set_label_taxonomy", rpc_set_label_taxonomy, 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, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
||||
}
|
||||
|
|
@ -2620,6 +2688,9 @@ static bool resolve_state_dir(daemon_state *state, const char *override) {
|
|||
if ((size_t)snprintf(state->taxonomy_file, sizeof state->taxonomy_file,
|
||||
"%s/labels.json", dir) >= sizeof state->taxonomy_file)
|
||||
return false;
|
||||
if ((size_t)snprintf(state->data_dir, sizeof state->data_dir, "%s", dir) >=
|
||||
sizeof state->data_dir)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2727,6 +2798,7 @@ int main(int argc, char **argv) {
|
|||
load_script_settings(&state); /* user-set values; schema comes from the script */
|
||||
pthread_mutex_init(&state.taxonomy_lock, NULL);
|
||||
load_taxonomy(&state); /* persisted category + tag lists for the web UI */
|
||||
pthread_mutex_init(&state.blob_lock, NULL);
|
||||
state.events = naut_event_bus_create();
|
||||
state.rpc = naut_rpc_registry_create();
|
||||
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
||||
|
|
@ -2812,5 +2884,6 @@ int main(int argc, char **argv) {
|
|||
json_decref(state.label_categories);
|
||||
json_decref(state.label_tags);
|
||||
pthread_mutex_destroy(&state.taxonomy_lock);
|
||||
pthread_mutex_destroy(&state.blob_lock);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue