From 0249ce330d1527a9bd9faa22585ac944fd2cecd1 Mon Sep 17 00:00:00 2001 From: ookami125 Date: Tue, 23 Jun 2026 01:45:49 -0400 Subject: [PATCH] =?UTF-8?q?webui:=20RSS=20manual=20download,=20repull,=20a?= =?UTF-8?q?nd=20rule=20re-run=20(#13=E2=80=93#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Articles now carry a `grabbed` flag; manually downloading one (or an auto/forced rule grab) marks it, persisted across restarts, so the UI shows it as added and re-runs skip it. - POST /api/rss/refresh {name?} synchronously re-polls one feed (or all) for an on-demand repull. - POST /api/rss/rules/run {name} re-applies a rule to every existing article (not just newly-seen ones), grabbing ungrabbed matches — useful after editing a rule. Returns matched/grabbed counts. Co-Authored-By: Claude Opus 4.8 --- ISSUES.md | 5 +- plugins/webui/webui.c | 134 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/ISSUES.md b/ISSUES.md index 79b8a25..9c96f9d 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -9,4 +9,7 @@ - ✅ Pausing a torrent will go back into Downloading and Seeding. - ✅ A torrents data could overlap with another existing torrent. This should be blocked to avoid - ✅ A paused torrent should still do a full piece check. -- ✅ Something appears to have broken the peers info tab, nothing shows up. \ No newline at end of file +- ✅ Something appears to have broken the peers info tab, nothing shows up. +- ✅ RSS should have a manual download button +- ✅ RSS should have a manual repull +- ✅ I should be able to force re-run a rule for cases where it was modified. \ No newline at end of file diff --git a/plugins/webui/webui.c b/plugins/webui/webui.c index 72d397a..f8b0bd9 100644 --- a/plugins/webui/webui.c +++ b/plugins/webui/webui.c @@ -2019,9 +2019,35 @@ static bool rule_matches(json_t *rule, const char *feed_name, const char *title) return true; } +/* Mark the article with this key as grabbed (across all feeds), so an + * auto-download rule re-run won't fetch it again. */ +static void rss_mark_grabbed(const char *key) { + if (!key || !*key) return; + pthread_mutex_lock(&g_webui.rss_lock); + size_t fi; json_t *feed; + json_array_foreach(g_webui.rss_feeds, fi, feed) { + json_t *articles = json_object_get(feed, "articles"); + size_t ai; json_t *a; + json_array_foreach(articles, ai, a) + if (strcmp(json_string_or(a, "key", ""), key) == 0) + json_object_set_new(a, "grabbed", json_true()); + } + pthread_mutex_unlock(&g_webui.rss_lock); +} + +/* Download an article and, on success, flag it grabbed by key. */ +static bool rss_grab_article(const char *key, const char *magnet, + const char *torrent_url, const char *cat, + const char *path, bool paused) { + bool ok = rss_download(magnet, torrent_url, cat, path, paused); + if (ok) rss_mark_grabbed(key); + return ok; +} + /* Run every rule against a freshly-seen article; download the first match. */ -static void rss_run_rules(const char *feed_name, const char *title, - const char *magnet, const char *torrent_url) { +static void rss_run_rules(const char *feed_name, const char *key, + const char *title, const char *magnet, + const char *torrent_url) { size_t i; json_t *rule; json_t *fire = NULL; char cat[128] = {0}, path[1024] = {0}; bool paused = false; pthread_mutex_lock(&g_webui.rss_lock); @@ -2037,7 +2063,7 @@ static void rss_run_rules(const char *feed_name, const char *title, } pthread_mutex_unlock(&g_webui.rss_lock); if (!fire) return; - if (rss_download(magnet, torrent_url, cat, path, paused)) + if (rss_grab_article(key, magnet, torrent_url, cat, path, paused)) log_msg(2, "rss: auto-downloaded a match"); } @@ -2086,16 +2112,16 @@ static int rss_ingest(json_t *feed, const char *xml, size_t len, json_t *out_new } if (!seen) { json_t *art = json_pack( - "{s:s,s:s,s:s,s:s,s:I,s:s,s:b}", + "{s:s,s:s,s:s,s:s,s:I,s:s,s:b,s:b}", "title", title, "key", key, "magnet", magnet, "torrentUrl", enclosure, "size", (json_int_t)strtoll(lenstr, NULL, 10), - "pubDate", pub, "isRead", 0); + "pubDate", pub, "isRead", 0, "grabbed", 0); json_array_insert_new(articles, 0, art); added++; if (out_new) json_array_append_new(out_new, json_pack( - "{s:s,s:s,s:s}", "title", title, + "{s:s,s:s,s:s,s:s}", "title", title, "key", key, "magnet", magnet, "torrentUrl", enclosure)); } } @@ -2134,7 +2160,8 @@ static void rss_poll_feed_by_index(size_t idx) { /* fire auto-download rules now that rss_lock is released */ size_t i; json_t *a; json_array_foreach(new_articles, i, a) - rss_run_rules(feed_name, json_string_or(a, "title", ""), + rss_run_rules(feed_name, json_string_or(a, "key", ""), + json_string_or(a, "title", ""), json_string_or(a, "magnet", ""), json_string_or(a, "torrentUrl", "")); json_decref(new_articles); @@ -2263,6 +2290,66 @@ static void api_rss_rule(int fd, const char *body, size_t len, bool remove) { api_rss_rules_list(fd); } +/* POST /api/rss/rules/run {name} — re-apply a rule to every article already in + * the feeds (not just newly-seen ones), downloading matches not yet grabbed. + * Used after editing a rule. Runs regardless of the rule's enabled flag. */ +static void api_rss_rule_run(int fd, const char *body, size_t len) { + json_t *req = read_body_json(body, len); + const char *name = json_string_value(json_object_get(req, "name")); + char cat[128] = {0}, path[1024] = {0}; bool paused = false; + json_t *todo = json_array(); /* {key,magnet,torrentUrl} to grab */ + + pthread_mutex_lock(&g_webui.rss_lock); + json_t *rule = NULL; size_t i; json_t *r; + if (name) json_array_foreach(g_webui.rss_rules, i, r) + if (strcmp(json_string_or(r, "name", ""), name) == 0) { rule = r; break; } + if (rule) { + snprintf(cat, sizeof cat, "%s", json_string_or(rule, "assignedCategory", "")); + snprintf(path, sizeof path, "%s", json_string_or(rule, "savePath", "")); + paused = json_boolean_value(json_object_get(rule, "addPaused")); + /* match regardless of the enabled flag (explicit manual run) */ + json_t *probe = json_deep_copy(rule); + json_object_set_new(probe, "enabled", json_true()); + size_t fi; json_t *feed; + json_array_foreach(g_webui.rss_feeds, fi, feed) { + const char *fname = json_string_or(feed, "name", ""); + json_t *articles = json_object_get(feed, "articles"); + size_t ai; json_t *a; + json_array_foreach(articles, ai, a) { + if (json_boolean_value(json_object_get(a, "grabbed"))) continue; + const char *mag = json_string_or(a, "magnet", ""); + const char *url = json_string_or(a, "torrentUrl", ""); + if (!*mag && !*url) continue; + if (rule_matches(probe, fname, json_string_or(a, "title", ""))) + json_array_append_new(todo, json_pack("{s:s,s:s,s:s}", + "key", json_string_or(a, "key", ""), "magnet", mag, "torrentUrl", url)); + } + } + json_decref(probe); + if (json_array_size(todo)) + json_object_set_new(rule, "lastMatch", json_integer((json_int_t)time(NULL))); + } + bool found = rule != NULL; + pthread_mutex_unlock(&g_webui.rss_lock); + json_decref(req); + + int grabbed = 0; + size_t j; json_t *t; + json_array_foreach(todo, j, t) + if (rss_grab_article(json_string_or(t, "key", ""), json_string_or(t, "magnet", ""), + json_string_or(t, "torrentUrl", ""), cat, path, paused)) + grabbed++; + size_t matched = json_array_size(todo); + json_decref(todo); + if (grabbed > 0) rss_save(); + + if (!found) { http_text(fd, 404, "Not Found", "no such rule"); return; } + json_t *reply = json_pack("{s:b,s:i,s:i}", "ok", 1, + "matched", (int)matched, "grabbed", grabbed); + http_json(fd, 200, reply); + json_decref(reply); +} + /* POST /api/rss/download {magnet|torrentUrl, category, savePath, paused} * Manually grab a torrent from a feed article or search result. Reuses the * same add path as the auto-downloader (handles magnets and .torrent URLs). */ @@ -2272,8 +2359,10 @@ static void api_rss_download(int fd, const char *body, size_t len) { const char *url = json_string_or(req, "torrentUrl", ""); const char *cat = json_string_or(req, "category", ""); const char *path = json_string_or(req, "savePath", ""); + const char *key = json_string_or(req, "key", ""); bool paused = json_boolean_value(json_object_get(req, "paused")); - bool ok = rss_download(magnet, url, cat, path, paused); + bool ok = rss_grab_article(key, magnet, url, cat, path, paused); + if (ok && *key) rss_save(); /* persist the grabbed flag */ json_decref(req); if (ok) { json_t *reply = json_pack("{s:b}", "ok", 1); @@ -2284,6 +2373,31 @@ static void api_rss_download(int fd, const char *body, size_t len) { } } +/* POST /api/rss/refresh {name?} — re-poll a feed now (or all feeds), running + * the network fetch synchronously so the response reflects fresh articles. */ +static void api_rss_refresh(int fd, const char *body, size_t len) { + json_t *req = read_body_json(body, len); + const char *name = json_string_value(json_object_get(req, "name")); + /* find matching index(es) under the lock, then poll outside it */ + pthread_mutex_lock(&g_webui.rss_lock); + size_t n = json_array_size(g_webui.rss_feeds); + int target = -1; + if (name && *name) { + size_t i; json_t *fd_j; + json_array_foreach(g_webui.rss_feeds, i, fd_j) + if (strcmp(json_string_or(fd_j, "name", ""), name) == 0) { target = (int)i; break; } + } + pthread_mutex_unlock(&g_webui.rss_lock); + json_decref(req); + if (target >= 0) { + rss_poll_feed_by_index((size_t)target); + } else if (!name || !*name) { + for (size_t i = 0; i < n && !atomic_load(&g_webui.stopping); i++) + rss_poll_feed_by_index(i); + } + api_rss_list(fd); +} + /* POST /api/indexers upserts a Torznab indexer; .../delete removes one. */ static void api_indexer(int fd, const char *body, size_t len, bool remove) { json_t *req = read_body_json(body, len); @@ -2650,6 +2764,10 @@ static void handle_api(int fd, const char *method, char *path, api_rss_rule(fd, body, body_len, false); } else if (strcmp(path, "/api/rss/rules/delete") == 0 && strcmp(method, "POST") == 0) { api_rss_rule(fd, body, body_len, true); + } else if (strcmp(path, "/api/rss/rules/run") == 0 && strcmp(method, "POST") == 0) { + api_rss_rule_run(fd, body, body_len); + } else if (strcmp(path, "/api/rss/refresh") == 0 && strcmp(method, "POST") == 0) { + api_rss_refresh(fd, body, body_len); } else if (strcmp(path, "/api/rss/download") == 0 && strcmp(method, "POST") == 0) { api_rss_download(fd, body, body_len); } else if (strcmp(path, "/api/indexers") == 0 && strcmp(method, "POST") == 0) {