webui: edit categories (rename + save path, incl. Uncategorized)

Add POST /api/categories/edit. Renames a category, updating its save
path and reassigning every torrent that referenced the old name; the
empty-named Uncategorized pseudo-category can have its save path set
but not renamed. Changes persist via the daemon taxonomy.

Closes ISSUES #2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-23 00:29:16 -04:00
parent 4708db151d
commit f6c933fd1c
2 changed files with 50 additions and 1 deletions

View file

@ -1,5 +1,5 @@
- ✅ Set Location doesn't work, it should also show the current location.
- I need a way to modify a category (including Uncategorozied).
- I need a way to modify a category (including Uncategorozied).
- ✅ Adding a Category means I can't have none selected on adding a torrent.
- ⬛ Category default download location does nothing as changing it doesn't change the download location. Download location should be greyed out by default with the default location shown. Clicking should allow you to change the location. If the location is set it shouldn't change if the category is changed.
- ✅ Automation variables at half window width makes the script unseeable. It should be displayed above the script if the window is to narrow.

View file

@ -1734,6 +1734,52 @@ static void api_categories(int fd, const char *body, size_t len, bool remove) {
json_decref(reply);
}
/* POST /api/categories/edit — rename a category and/or change its save path,
* reassigning every torrent that referenced the old name. The empty-named
* "Uncategorized" pseudo-category can have its savePath set but not renamed. */
static void api_category_edit(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"));
const char *new_name = json_string_value(json_object_get(req, "newName"));
const char *save_path = json_string_value(json_object_get(req, "savePath"));
if (!name) name = "";
if (!save_path) save_path = "";
bool rename = new_name && *new_name && *name && strcmp(new_name, name) != 0;
const char *target = rename ? new_name : name;
pthread_mutex_lock(&g_webui.meta_lock);
int idx = find_category(name);
if (idx >= 0) {
json_t *cat = json_array_get(g_webui.categories, (size_t)idx);
json_object_set_new(cat, "name", json_string(target));
json_object_set_new(cat, "savePath", json_string(save_path));
} else if (*target || *save_path) {
/* The category didn't exist yet (e.g. setting Uncategorized's path, or
* editing a name that was only implied by assignments). An empty name
* is allowed here: it holds Uncategorized's default save path. */
json_array_append_new(g_webui.categories, json_pack(
"{s:s,s:s}", "name", target, "savePath", save_path));
}
if (rename) {
const char *key;
json_t *entry;
json_object_foreach(g_webui.assignments, key, entry)
if (strcmp(json_string_or(entry, "category", ""), name) == 0)
json_object_set_new(entry, "category", json_string(target));
}
pthread_mutex_unlock(&g_webui.meta_lock);
if (rename) webui_sync_all_labels();
webui_sync_taxonomy();
json_decref(req);
pthread_mutex_lock(&g_webui.meta_lock);
json_t *reply = json_deep_copy(g_webui.categories);
pthread_mutex_unlock(&g_webui.meta_lock);
http_json(fd, 200, reply);
json_decref(reply);
}
/* POST /api/tags and /api/tags/delete */
static void api_tags(int fd, const char *body, size_t len, bool remove) {
json_t *req = read_body_json(body, len);
@ -1877,6 +1923,9 @@ static void handle_api(int fd, const char *method, char *path,
} else if (strcmp(path, "/api/categories/delete") == 0 &&
strcmp(method, "POST") == 0) {
api_categories(fd, body, body_len, true);
} else if (strcmp(path, "/api/categories/edit") == 0 &&
strcmp(method, "POST") == 0) {
api_category_edit(fd, body, body_len);
} else if (strcmp(path, "/api/tags") == 0 &&
strcmp(method, "POST") == 0) {
api_tags(fd, body, body_len, false);