nautd/webui: scripting, labels, settings, set-location, pause fix
Session checkpoint on webui-plugin: - engine dump (nautctl dump) + engine endgame integration - per-file move locations persistence; torrent-level "Set location" with reset/keep-relative/leave-separate handling + residual prune - Lua: naut.get_labels, define_settings/get_setting (script_host struct) - daemon-owned labels (category+tags) + taxonomy persistence; webui write-through - fix: pausing a completed/seeding torrent now sticks (stop wins over result) - automation tab responsive layout; anime_sort label gating + settings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6dc711cf57
commit
b633b7d216
40 changed files with 3305 additions and 3267 deletions
|
|
@ -442,6 +442,13 @@ static uint64_t json_u64(const json_t *obj, const char *key) {
|
|||
? (uint64_t)json_integer_value(value) : 0;
|
||||
}
|
||||
|
||||
static int64_t json_i64_or(const json_t *obj, const char *key,
|
||||
int64_t fallback) {
|
||||
json_t *value = json_object_get(obj, key);
|
||||
return json_is_integer(value) ? (int64_t)json_integer_value(value)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
static double json_number_or(const json_t *obj, const char *key,
|
||||
double fallback) {
|
||||
json_t *value = json_object_get(obj, key);
|
||||
|
|
@ -456,6 +463,11 @@ static const char *base_name(const char *path) {
|
|||
}
|
||||
|
||||
static char *torrent_name(const json_t *torrent) {
|
||||
/* The daemon persists the display name captured at add time; prefer it so
|
||||
* restored torrents (where the in-process name cache is empty) read right
|
||||
* instead of falling back to an upload path's basename. */
|
||||
const char *saved = json_string_value(json_object_get(torrent, "name"));
|
||||
if (saved && *saved) return strdup(saved);
|
||||
const char *source = json_string_or(torrent, "source", "torrent");
|
||||
if (strncmp(source, "magnet:", 7) == 0) {
|
||||
const char *dn = strstr(source, "dn=");
|
||||
|
|
@ -475,9 +487,13 @@ static char *torrent_name(const json_t *torrent) {
|
|||
static const char *ui_state(const char *state, double progress) {
|
||||
if (!state) return "stalledDL";
|
||||
if (strcmp(state, "complete") == 0) return "uploading";
|
||||
if (strcmp(state, "paused") == 0)
|
||||
return progress >= 1.0 ? "pausedUP" : "pausedDL";
|
||||
if (strcmp(state, "stopped") == 0)
|
||||
return progress >= 1.0 ? "pausedUP" : "pausedDL";
|
||||
if (strcmp(state, "stopping") == 0) return "pausedDL";
|
||||
if (strcmp(state, "stalled") == 0)
|
||||
return progress >= 1.0 ? "stalledUP" : "stalledDL";
|
||||
if (strcmp(state, "queued") == 0) return "queuedDL";
|
||||
if (strcmp(state, "error") == 0) return "error";
|
||||
return "downloading";
|
||||
|
|
@ -786,6 +802,89 @@ static void store_set_name(uint64_t id, const char *name) {
|
|||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
}
|
||||
|
||||
static bool parse_id(const char *text, uint64_t *id);
|
||||
|
||||
/* Mirror a torrent's category + tags into the daemon (which persists them and
|
||||
* exposes the flattened set to Lua via naut.get_labels). The web layer is the
|
||||
* editing surface; the daemon is the source of truth. Snapshots the assignment
|
||||
* under meta_lock, then RPCs without it held. */
|
||||
static void webui_sync_labels(uint64_t id) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
json_t *entry = json_object_get(g_webui.assignments, key);
|
||||
char *category = strdup(entry ? json_string_or(entry, "category", "") : "");
|
||||
json_t *tags_src = entry ? json_object_get(entry, "tags") : NULL;
|
||||
json_t *tags = tags_src ? json_deep_copy(tags_src) : json_array();
|
||||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
|
||||
json_t *params = json_pack("{s:I,s:s,s:o}", "torrent_id", (json_int_t)id,
|
||||
"category", category ? category : "",
|
||||
"tags", tags);
|
||||
free(category);
|
||||
if (!params) { json_decref(tags); return; }
|
||||
json_t *reply = rpc_call_json("set_labels", params);
|
||||
json_decref(params);
|
||||
if (reply) json_decref(reply);
|
||||
}
|
||||
|
||||
/* Re-push every torrent's labels (after a global category/tag removal that can
|
||||
* touch many assignments at once). */
|
||||
static void webui_sync_all_labels(void) {
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
size_t n = json_object_size(g_webui.assignments);
|
||||
uint64_t *ids = n ? malloc(n * sizeof *ids) : NULL;
|
||||
size_t count = 0;
|
||||
if (ids) {
|
||||
const char *key;
|
||||
json_t *entry;
|
||||
json_object_foreach(g_webui.assignments, key, entry) {
|
||||
uint64_t id = 0;
|
||||
if (parse_id(key, &id)) ids[count++] = id;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
for (size_t i = 0; i < count; i++) webui_sync_labels(ids[i]);
|
||||
free(ids);
|
||||
}
|
||||
|
||||
/* Persist the full category + tag lists (including unassigned ones) to the
|
||||
* daemon so they survive restarts. */
|
||||
static void webui_sync_taxonomy(void) {
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
json_t *cats = json_deep_copy(g_webui.categories);
|
||||
json_t *tags = json_deep_copy(g_webui.tags);
|
||||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
json_t *params = json_pack("{s:o,s:o}",
|
||||
"categories", cats ? cats : json_array(),
|
||||
"tags", tags ? tags : json_array());
|
||||
if (!params) { json_decref(cats); json_decref(tags); return; }
|
||||
json_t *reply = rpc_call_json("set_label_taxonomy", params);
|
||||
json_decref(params);
|
||||
if (reply) json_decref(reply);
|
||||
}
|
||||
|
||||
/* Seed the category + tag lists from the daemon's persisted copy at startup. */
|
||||
static void webui_load_taxonomy(void) {
|
||||
json_t *params = json_object();
|
||||
json_t *reply = rpc_call_json("get_label_taxonomy", params);
|
||||
json_decref(params);
|
||||
if (!json_is_object(reply)) { json_decref(reply); return; }
|
||||
json_t *cats = json_object_get(reply, "categories");
|
||||
json_t *tags = json_object_get(reply, "tags");
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
if (json_is_array(cats)) {
|
||||
json_decref(g_webui.categories);
|
||||
g_webui.categories = json_deep_copy(cats);
|
||||
}
|
||||
if (json_is_array(tags)) {
|
||||
json_decref(g_webui.tags);
|
||||
g_webui.tags = json_deep_copy(tags);
|
||||
}
|
||||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
json_decref(reply);
|
||||
}
|
||||
|
||||
static bool store_get_name(uint64_t id, char *out, size_t out_size) {
|
||||
bool found = false;
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
|
|
@ -808,6 +907,35 @@ static void store_forget(uint64_t id) {
|
|||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
}
|
||||
|
||||
/* On first sight of a torrent (e.g. right after a restart, when the in-memory
|
||||
* store is empty), seed its assignment + the global category/tag lists from the
|
||||
* daemon's persisted category/tags. Only creates a missing entry, so live web
|
||||
* edits are never clobbered. */
|
||||
static void seed_assignment_from_daemon(uint64_t id, json_t *torrent) {
|
||||
const char *category = json_string_or(torrent, "category", "");
|
||||
json_t *tags = json_object_get(torrent, "tags");
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
if (!json_object_get(g_webui.assignments, key)) {
|
||||
json_t *entry = json_pack(
|
||||
"{s:s,s:o}", "category", category,
|
||||
"tags", json_is_array(tags) ? json_deep_copy(tags) : json_array());
|
||||
if (entry) json_object_set_new(g_webui.assignments, key, entry);
|
||||
if (category && *category && find_category(category) < 0)
|
||||
json_array_append_new(g_webui.categories, json_pack(
|
||||
"{s:s,s:s}", "name", category, "savePath", ""));
|
||||
size_t i;
|
||||
json_t *v;
|
||||
json_array_foreach(tags, i, v) {
|
||||
const char *t = json_string_value(v);
|
||||
if (t && *t && find_tag(t) < 0)
|
||||
json_array_append_new(g_webui.tags, json_string(t));
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||
}
|
||||
|
||||
/* Fill in category + tags for a torrent from the assignment store. */
|
||||
static void apply_assignment(json_t *out, uint64_t id) {
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
|
|
@ -838,9 +966,13 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
|||
char *better = strdup(override);
|
||||
if (better) { free(name); name = better; }
|
||||
}
|
||||
bool force_start =
|
||||
json_boolean_value(json_object_get(torrent, "force_start"));
|
||||
const char *state = ui_state(json_string_value(json_object_get(torrent,
|
||||
"state")),
|
||||
progress);
|
||||
if (force_start && strcmp(state, "downloading") == 0)
|
||||
state = progress >= 1.0 ? "forcedUP" : "forcedDL";
|
||||
|
||||
json_t *trackers = NULL;
|
||||
json_t *files = NULL;
|
||||
|
|
@ -899,18 +1031,24 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
|||
json_object_set_new(out, "downloaded", json_integer((json_int_t)done));
|
||||
json_object_set_new(out, "uploaded", json_integer(0));
|
||||
json_object_set_new(out, "availability", json_real(1.0));
|
||||
json_object_set_new(out, "priority", json_integer(1));
|
||||
int64_t queue_pos = json_i64_or(torrent, "queue_pos", 0);
|
||||
json_object_set_new(out, "priority",
|
||||
json_integer((json_int_t)(queue_pos < 0
|
||||
? 1 : queue_pos + 1)));
|
||||
json_object_set_new(out, "queuePos", json_integer((json_int_t)queue_pos));
|
||||
json_object_set_new(out, "trackerHosts", hosts ? hosts : json_array());
|
||||
json_object_set_new(out, "seqDl", json_false());
|
||||
json_object_set_new(out, "superSeeding", json_false());
|
||||
json_object_set_new(out, "forceStart", json_false());
|
||||
json_object_set_new(out, "forceStart", json_boolean(force_start));
|
||||
json_object_set_new(out, "timeActive",
|
||||
json_integer((json_int_t)json_u64(torrent, "elapsed_seconds")));
|
||||
json_object_set_new(out, "pieceSize",
|
||||
json_integer(pieces ? (json_int_t)(total / pieces) : 0));
|
||||
json_object_set_new(out, "state", json_string(state));
|
||||
json_object_set_new(out, "contentPath", json_string(output));
|
||||
/* category + tags come from the web-layer assignment store */
|
||||
/* Seed the web-layer store from the daemon's persisted category/tags the
|
||||
* first time we see a torrent (survives restarts), then apply it. */
|
||||
seed_assignment_from_daemon(id, torrent);
|
||||
apply_assignment(out, id);
|
||||
if (detail) {
|
||||
json_object_set_new(out, "comment", json_string(""));
|
||||
|
|
@ -934,6 +1072,8 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
|||
return out;
|
||||
}
|
||||
|
||||
static json_t *preferences_json(void);
|
||||
|
||||
/* Build a fresh snapshot (grid + global stats) with live download rates. */
|
||||
static json_t *build_snapshot(void) {
|
||||
json_t *params = json_object();
|
||||
|
|
@ -945,6 +1085,7 @@ static json_t *build_snapshot(void) {
|
|||
}
|
||||
speed_retain(torrents);
|
||||
|
||||
json_t *prefs = preferences_json();
|
||||
json_t *items = json_array();
|
||||
uint64_t active = 0;
|
||||
uint64_t total_rate = 0;
|
||||
|
|
@ -958,20 +1099,35 @@ static json_t *build_snapshot(void) {
|
|||
json_t *mapped = map_torrent(torrent, false, dlspeed);
|
||||
if (!mapped) continue;
|
||||
const char *state = json_string_value(json_object_get(mapped, "state"));
|
||||
if (state && strcmp(state, "downloading") == 0) active++;
|
||||
if (state && (strcmp(state, "downloading") == 0 ||
|
||||
strcmp(state, "forcedDL") == 0)) active++;
|
||||
total_rate += (uint64_t)dlspeed;
|
||||
total_data += done;
|
||||
json_array_append_new(items, mapped);
|
||||
int64_t q = json_i64_or(mapped, "queuePos", 0);
|
||||
size_t pos = 0;
|
||||
for (; pos < json_array_size(items); pos++) {
|
||||
json_t *cur = json_array_get(items, pos);
|
||||
if (q < json_i64_or(cur, "queuePos", 0)) break;
|
||||
}
|
||||
if (json_array_insert_new(items, pos, mapped) != 0)
|
||||
json_decref(mapped);
|
||||
}
|
||||
json_decref(torrents);
|
||||
bool alt_speed = prefs &&
|
||||
json_boolean_value(json_object_get(prefs, "alt_speed_enabled"));
|
||||
uint64_t dl_limit = prefs ? json_u64(prefs, alt_speed ? "alt_dl_limit"
|
||||
: "dl_limit") : 0;
|
||||
uint64_t up_limit = prefs ? json_u64(prefs, alt_speed ? "alt_up_limit"
|
||||
: "up_limit") : 0;
|
||||
json_t *server = json_pack(
|
||||
"{s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
|
||||
"{s:I,s:i,s:I,s:i,s:I,s:I,s:b,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
|
||||
"dl_info_speed", (json_int_t)total_rate,
|
||||
"up_info_speed", 0,
|
||||
"dl_info_data", (json_int_t)total_data,
|
||||
"up_info_data", 0,
|
||||
"dl_rate_limit", 0,
|
||||
"up_rate_limit", 0,
|
||||
"dl_rate_limit", (json_int_t)dl_limit,
|
||||
"up_rate_limit", (json_int_t)up_limit,
|
||||
"alt_speed_enabled", alt_speed,
|
||||
"global_ratio", 0.0,
|
||||
"dht_nodes", 0,
|
||||
"connection_status", "connected",
|
||||
|
|
@ -981,6 +1137,7 @@ static json_t *build_snapshot(void) {
|
|||
"total_torrents", (int)json_array_size(items),
|
||||
"read_cache_hits", "0.0",
|
||||
"queued_io_jobs", 0);
|
||||
json_decref(prefs);
|
||||
return json_pack("{s:I,s:o,s:o}", "ts", (json_int_t)time(NULL) * 1000,
|
||||
"server", server, "torrents", items);
|
||||
}
|
||||
|
|
@ -1043,9 +1200,49 @@ static json_t *full_torrent_by_hash(const char *hash) {
|
|||
return mapped;
|
||||
}
|
||||
|
||||
static json_t *preferences_json(void) {
|
||||
json_t *params = json_object();
|
||||
json_t *prefs = rpc_call_json("get_preferences", params);
|
||||
json_decref(params);
|
||||
if (!json_is_object(prefs)) {
|
||||
json_decref(prefs);
|
||||
prefs = json_object();
|
||||
}
|
||||
if (!prefs) return NULL;
|
||||
|
||||
json_t *max_active = json_object_get(prefs, "max_active");
|
||||
if (json_is_integer(max_active) &&
|
||||
!json_object_get(prefs, "max_active_downloads")) {
|
||||
json_object_set_new(prefs, "max_active_downloads",
|
||||
json_integer(json_integer_value(max_active)));
|
||||
}
|
||||
if (!json_object_get(prefs, "save_path"))
|
||||
json_object_set_new(prefs, "save_path",
|
||||
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
|
||||
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
|
||||
if (!json_object_get(prefs, "dl_limit"))
|
||||
json_object_set_new(prefs, "dl_limit", json_integer(0));
|
||||
if (!json_object_get(prefs, "up_limit"))
|
||||
json_object_set_new(prefs, "up_limit", json_integer(0));
|
||||
if (!json_object_get(prefs, "alt_dl_limit"))
|
||||
json_object_set_new(prefs, "alt_dl_limit", json_integer(0));
|
||||
if (!json_object_get(prefs, "alt_up_limit"))
|
||||
json_object_set_new(prefs, "alt_up_limit", json_integer(0));
|
||||
if (!json_object_get(prefs, "alt_speed_enabled"))
|
||||
json_object_set_new(prefs, "alt_speed_enabled", json_false());
|
||||
json_object_set_new(prefs, "max_connec", json_integer(500));
|
||||
json_object_set_new(prefs, "max_connec_per_torrent", json_integer(100));
|
||||
json_object_set_new(prefs, "max_uploads", json_integer(20));
|
||||
json_object_set_new(prefs, "max_active_uploads", json_integer(10));
|
||||
json_object_set_new(prefs, "max_active_torrents",
|
||||
json_integer((json_int_t)json_i64_or(
|
||||
prefs, "max_active_downloads", 5)));
|
||||
return prefs;
|
||||
}
|
||||
|
||||
static void api_meta(int fd) {
|
||||
json_t *json = json_object();
|
||||
json_t *preferences = json_object();
|
||||
json_t *preferences = preferences_json();
|
||||
if (!json || !preferences) {
|
||||
json_decref(json);
|
||||
json_decref(preferences);
|
||||
|
|
@ -1063,12 +1260,6 @@ static void api_meta(int fd) {
|
|||
json_t *trackers = tracker_summary(torrents);
|
||||
json_decref(torrents);
|
||||
json_object_set_new(json, "trackers", trackers ? trackers : json_array());
|
||||
json_object_set_new(preferences, "save_path",
|
||||
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
|
||||
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
|
||||
json_object_set_new(preferences, "dl_limit", json_integer(0));
|
||||
json_object_set_new(preferences, "up_limit", json_integer(0));
|
||||
json_object_set_new(preferences, "alt_speed_enabled", json_false());
|
||||
json_object_set_new(json, "preferences", preferences);
|
||||
json_object_set_new(json, "searchPlugins", json_array());
|
||||
http_json(fd, 200, json);
|
||||
|
|
@ -1190,6 +1381,13 @@ static void api_add(int fd, const char *body, size_t len) {
|
|||
json_object_set_new(params, "output", json_string(save_path));
|
||||
if (source && *source) json_object_set_new(params, "source", json_string(source));
|
||||
if (data && *data) json_object_set_new(params, "data", json_string(data));
|
||||
if (json_object_get(req, "paused"))
|
||||
json_object_set_new(params, "paused",
|
||||
json_boolean(json_boolean_value(
|
||||
json_object_get(req, "paused"))));
|
||||
/* Forward the display name so the daemon persists it for restore. */
|
||||
if (display_name[0])
|
||||
json_object_set_new(params, "name", json_string(display_name));
|
||||
json_t *result = rpc_call_json("add_torrent", params);
|
||||
json_decref(params);
|
||||
json_decref(req);
|
||||
|
|
@ -1236,10 +1434,33 @@ static void api_delete(int fd, const char *body, size_t len) {
|
|||
if (removed) publish_snapshot();
|
||||
}
|
||||
|
||||
/* Category/tag assignment is web-layer state the plugin owns, so those verbs
|
||||
* are honored here. Engine-level verbs (pause/resume/recheck/queue/rate
|
||||
* limits) have no daemon support yet, so we return 501 instead of pretending
|
||||
* they worked; the front end shows that as an honest "Action failed" toast. */
|
||||
static bool rpc_for_torrent(const char *method, uint64_t id, json_t *extra) {
|
||||
json_t *params = json_object();
|
||||
if (!params) return false;
|
||||
json_object_set_new(params, "torrent_id", json_integer((json_int_t)id));
|
||||
if (json_is_object(extra)) {
|
||||
const char *key;
|
||||
json_t *value;
|
||||
json_object_foreach(extra, key, value)
|
||||
json_object_set(params, key, value);
|
||||
}
|
||||
json_t *result = rpc_call_json(method, params);
|
||||
json_decref(params);
|
||||
if (!result) return false;
|
||||
json_decref(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *queue_op_for_action(const char *action) {
|
||||
if (strcmp(action, "topPriority") == 0) return "top";
|
||||
if (strcmp(action, "bottomPriority") == 0) return "bottom";
|
||||
if (strcmp(action, "increasePriority") == 0) return "up";
|
||||
if (strcmp(action, "decreasePriority") == 0) return "down";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Category/tag assignment is web-layer state. Engine-backed verbs delegate to
|
||||
* nautd RPCs so toolbar actions mutate the real queue/lifecycle state. */
|
||||
static void api_action(int fd, const char *body, size_t len) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *raw_action = json_string_value(json_object_get(req, "action"));
|
||||
|
|
@ -1248,6 +1469,7 @@ static void api_action(int fd, const char *body, size_t len) {
|
|||
json_t *hashes = json_object_get(req, "hashes");
|
||||
json_t *params = json_object_get(req, "params");
|
||||
bool handled = false;
|
||||
int affected = 0;
|
||||
if (json_is_array(hashes) &&
|
||||
(strcmp(action, "setCategory") == 0 ||
|
||||
strcmp(action, "addTags") == 0 ||
|
||||
|
|
@ -1263,12 +1485,54 @@ static void api_action(int fd, const char *body, size_t len) {
|
|||
else
|
||||
store_update_tags(id, json_object_get(params, "tags"),
|
||||
strcmp(action, "addTags") == 0);
|
||||
webui_sync_labels(id); /* mirror to the daemon (persist + Lua) */
|
||||
affected++;
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
if (json_is_array(hashes) && !handled) {
|
||||
const char *rpc = NULL;
|
||||
json_t *extra = NULL;
|
||||
if (strcmp(action, "pause") == 0) {
|
||||
rpc = "pause_torrent";
|
||||
} else if (strcmp(action, "resume") == 0) {
|
||||
rpc = "resume_torrent";
|
||||
} else if (strcmp(action, "forceStart") == 0) {
|
||||
rpc = "resume_torrent";
|
||||
extra = json_pack("{s:b}", "force", 1);
|
||||
} else if (strcmp(action, "recheck") == 0) {
|
||||
rpc = "recheck_torrent";
|
||||
} else if (strcmp(action, "setSavePath") == 0) {
|
||||
const char *sp =
|
||||
json_string_value(json_object_get(params, "savePath"));
|
||||
if (sp && *sp) {
|
||||
rpc = "set_save_path";
|
||||
extra = json_pack("{s:s,s:b}", "savePath", sp, "reset",
|
||||
json_boolean_value(
|
||||
json_object_get(params, "reset")));
|
||||
}
|
||||
} else {
|
||||
const char *op = queue_op_for_action(action);
|
||||
if (op) {
|
||||
rpc = "queue_move";
|
||||
extra = json_pack("{s:s}", "op", op);
|
||||
}
|
||||
}
|
||||
if (rpc) {
|
||||
size_t index;
|
||||
json_t *hash;
|
||||
json_array_foreach(hashes, index, hash) {
|
||||
uint64_t id = 0;
|
||||
if (!parse_id(json_string_value(hash), &id)) continue;
|
||||
if (rpc_for_torrent(rpc, id, extra)) affected++;
|
||||
}
|
||||
json_decref(extra);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
json_decref(req);
|
||||
if (handled) {
|
||||
json_t *json = json_pack("{s:b}", "ok", 1);
|
||||
json_t *json = json_pack("{s:b,s:i}", "ok", 1, "affected", affected);
|
||||
http_json(fd, 200, json);
|
||||
json_decref(json);
|
||||
publish_snapshot();
|
||||
|
|
@ -1282,14 +1546,143 @@ static void api_action(int fd, const char *body, size_t len) {
|
|||
json_decref(json);
|
||||
}
|
||||
|
||||
static void api_preferences(int fd, const char *method,
|
||||
const char *body, size_t len) {
|
||||
if (strcmp(method, "GET") == 0) {
|
||||
json_t *prefs = preferences_json();
|
||||
if (!prefs) {
|
||||
http_text(fd, 502, "Bad Gateway", "get_preferences failed");
|
||||
return;
|
||||
}
|
||||
http_json(fd, 200, prefs);
|
||||
json_decref(prefs);
|
||||
return;
|
||||
}
|
||||
if (strcmp(method, "POST") != 0) {
|
||||
http_text(fd, 405, "Method Not Allowed", "method not allowed");
|
||||
return;
|
||||
}
|
||||
json_t *req = read_body_json(body, len);
|
||||
json_t *params = json_object();
|
||||
if (!req || !params) {
|
||||
json_decref(req);
|
||||
json_decref(params);
|
||||
http_text(fd, 500, "Internal Server Error", "oom");
|
||||
return;
|
||||
}
|
||||
const char *keys[] = {
|
||||
"dl_limit", "up_limit", "alt_dl_limit", "alt_up_limit",
|
||||
"alt_speed_enabled", "max_active"
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
||||
json_t *v = json_object_get(req, keys[i]);
|
||||
if (v) json_object_set(params, keys[i], v);
|
||||
}
|
||||
json_t *max = json_object_get(req, "max_active_downloads");
|
||||
if (max) json_object_set(params, "max_active", max);
|
||||
json_t *result = rpc_call_json("set_preferences", params);
|
||||
json_decref(params);
|
||||
json_decref(req);
|
||||
if (!json_is_object(result)) {
|
||||
json_decref(result);
|
||||
http_text(fd, 502, "Bad Gateway", "set_preferences failed");
|
||||
return;
|
||||
}
|
||||
json_decref(result);
|
||||
json_t *prefs = preferences_json();
|
||||
http_json(fd, 200, prefs);
|
||||
json_decref(prefs);
|
||||
publish_snapshot();
|
||||
}
|
||||
|
||||
static void api_altspeed(int fd) {
|
||||
json_t *params = json_object();
|
||||
json_t *result = rpc_call_json("toggle_altspeed", params);
|
||||
json_decref(params);
|
||||
if (!json_is_object(result)) {
|
||||
json_decref(result);
|
||||
http_text(fd, 502, "Bad Gateway", "toggle_altspeed failed");
|
||||
return;
|
||||
}
|
||||
http_json(fd, 200, result);
|
||||
json_decref(result);
|
||||
publish_snapshot();
|
||||
}
|
||||
|
||||
/* POST /api/script/settings — persist user-edited script setting values. Body:
|
||||
* { "settings": { "<key>": "<value>", ... } }. */
|
||||
static void api_script_settings(int fd, const char *method, const char *body,
|
||||
size_t len) {
|
||||
if (strcmp(method, "POST") != 0) {
|
||||
http_text(fd, 405, "Method Not Allowed", "method not allowed");
|
||||
return;
|
||||
}
|
||||
json_t *req = read_body_json(body, len);
|
||||
json_t *settings = req ? json_object_get(req, "settings") : NULL;
|
||||
if (!json_is_object(settings)) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request", "missing settings object");
|
||||
return;
|
||||
}
|
||||
json_t *params = json_object();
|
||||
json_object_set(params, "settings", settings);
|
||||
json_decref(req);
|
||||
json_t *result = rpc_call_json("set_script_settings", params);
|
||||
json_decref(params);
|
||||
if (!json_is_object(result)) {
|
||||
json_decref(result);
|
||||
http_text(fd, 502, "Bad Gateway", "set_script_settings failed");
|
||||
return;
|
||||
}
|
||||
http_json(fd, 200, result);
|
||||
json_decref(result);
|
||||
}
|
||||
|
||||
static void api_script(int fd, const char *method, const char *body, size_t len) {
|
||||
json_t *params = NULL;
|
||||
json_t *result = NULL;
|
||||
const char *rpc_name = "script_status";
|
||||
if (strcmp(method, "GET") == 0) {
|
||||
params = json_object();
|
||||
result = rpc_call_json("script_status", params);
|
||||
} else if (strcmp(method, "POST") == 0) {
|
||||
rpc_name = "update_script";
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *source = json_string_value(json_object_get(req, "source"));
|
||||
if (!source) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request", "missing source");
|
||||
return;
|
||||
}
|
||||
params = json_object();
|
||||
json_object_set(params, "source", json_object_get(req, "source"));
|
||||
json_decref(req);
|
||||
result = rpc_call_json("update_script", params);
|
||||
} else {
|
||||
http_text(fd, 405, "Method Not Allowed", "method not allowed");
|
||||
return;
|
||||
}
|
||||
json_decref(params);
|
||||
if (!json_is_object(result)) {
|
||||
json_decref(result);
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof msg, "%s failed", rpc_name);
|
||||
http_text(fd, 502, "Bad Gateway", msg);
|
||||
return;
|
||||
}
|
||||
http_json(fd, 200, result);
|
||||
json_decref(result);
|
||||
}
|
||||
|
||||
/* POST /api/categories and /api/categories/delete */
|
||||
static void api_categories(int fd, const char *body, size_t len, bool remove) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *name = json_string_value(json_object_get(req, "name"));
|
||||
if (name && *name) {
|
||||
if (remove) store_remove_category(name);
|
||||
if (remove) { store_remove_category(name); webui_sync_all_labels(); }
|
||||
else store_add_category(name,
|
||||
json_string_value(json_object_get(req, "savePath")));
|
||||
webui_sync_taxonomy(); /* persist the category list via the daemon */
|
||||
}
|
||||
json_decref(req);
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
|
|
@ -1304,8 +1697,9 @@ static void api_tags(int fd, const char *body, size_t len, bool remove) {
|
|||
json_t *req = read_body_json(body, len);
|
||||
const char *name = json_string_value(json_object_get(req, "name"));
|
||||
if (name && *name) {
|
||||
if (remove) store_remove_tag(name);
|
||||
if (remove) { store_remove_tag(name); webui_sync_all_labels(); }
|
||||
else store_add_tag(name);
|
||||
webui_sync_taxonomy(); /* persist the tag list via the daemon */
|
||||
}
|
||||
json_decref(req);
|
||||
pthread_mutex_lock(&g_webui.meta_lock);
|
||||
|
|
@ -1428,11 +1822,13 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
|
||||
api_meta(fd);
|
||||
} else if (strcmp(path, "/api/preferences") == 0) {
|
||||
api_meta(fd);
|
||||
api_preferences(fd, method, body, body_len);
|
||||
} else if (strcmp(path, "/api/altspeed") == 0 && strcmp(method, "POST") == 0) {
|
||||
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
|
||||
http_json(fd, 200, json);
|
||||
json_decref(json);
|
||||
api_altspeed(fd);
|
||||
} else if (strcmp(path, "/api/script/settings") == 0) {
|
||||
api_script_settings(fd, method, body, body_len);
|
||||
} else if (strcmp(path, "/api/script") == 0) {
|
||||
api_script(fd, method, body, body_len);
|
||||
} else if (strcmp(path, "/api/categories") == 0 &&
|
||||
strcmp(method, "POST") == 0) {
|
||||
api_categories(fd, body, body_len, false);
|
||||
|
|
@ -1720,6 +2116,7 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
|||
if (error != NAUT_OK) goto fail_store;
|
||||
error = start_server();
|
||||
if (error != NAUT_OK) goto fail_store;
|
||||
webui_load_taxonomy(); /* restore category + tag lists from the daemon */
|
||||
return NAUT_OK;
|
||||
|
||||
fail_store:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue