foxygit / TransTUI Log in
commits tags

/src/torrent.c · 12.79 KB

raw
#include "torrent.h"
#include "util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---- small cJSON accessor helpers ---------------------------------- */

static int64_t get_i64(const cJSON *obj, const char *key, int64_t def)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key);
    if (!v || !cJSON_IsNumber(v))
        return def;
    return (int64_t)v->valuedouble;
}

static int get_int(const cJSON *obj, const char *key, int def)
{
    return (int)get_i64(obj, key, def);
}

static double get_dbl(const cJSON *obj, const char *key, double def)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key);
    if (!v || !cJSON_IsNumber(v))
        return def;
    return v->valuedouble;
}

static void get_str(const cJSON *obj, const char *key, char *buf, size_t n)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key);
    if (v && cJSON_IsString(v))
        snprintf(buf, n, "%s", v->valuestring);
    else
        buf[0] = '\0';
}

static cJSON *field_array(const char **names, size_t n)
{
    cJSON *arr = cJSON_CreateArray();
    for (size_t i = 0; i < n; i++)
        cJSON_AddItemToArray(arr, cJSON_CreateString(names[i]));
    return arr;
}

static cJSON *id_array(const int *ids, size_t n)
{
    cJSON *arr = cJSON_CreateArray();
    for (size_t i = 0; i < n; i++)
        cJSON_AddItemToArray(arr, cJSON_CreateNumber(ids[i]));
    return arr;
}

static void parse_summary(const cJSON *t, Torrent *out)
{
    memset(out, 0, sizeof(*out));
    out->id = get_int(t, "id", -1);
    get_str(t, "hashString", out->hash, sizeof(out->hash));
    get_str(t, "name", out->name, sizeof(out->name));
    out->total_size = get_i64(t, "totalSize", 0);
    out->left_until_done = get_i64(t, "leftUntilDone", 0);
    out->percent_done = get_dbl(t, "percentDone", 0.0);
    out->status = get_int(t, "status", TR_STATUS_STOPPED);
    out->rate_download = get_i64(t, "rateDownload", 0);
    out->rate_upload = get_i64(t, "rateUpload", 0);
    out->eta = get_int(t, "eta", -1);
    out->upload_ratio = get_dbl(t, "uploadRatio", 0.0);
    out->error = get_int(t, "error", 0);
    get_str(t, "errorString", out->error_string, sizeof(out->error_string));
    get_str(t, "downloadDir", out->download_dir, sizeof(out->download_dir));
    out->peers_connected = get_int(t, "peersConnected", 0);
    out->peers_sending_to_us = get_int(t, "peersSendingToUs", 0);
    out->peers_getting_from_us = get_int(t, "peersGettingFromUs", 0);
}

static const char *SUMMARY_FIELDS[] = {
    "id", "name", "hashString", "totalSize", "leftUntilDone", "percentDone",
    "status", "rateDownload", "rateUpload", "eta", "uploadRatio", "error",
    "errorString", "downloadDir", "peersConnected", "peersSendingToUs",
    "peersGettingFromUs",
};

void torrent_list_init(TorrentList *list)
{
    memset(list, 0, sizeof(*list));
}

void torrent_list_free(TorrentList *list)
{
    free(list->items);
    memset(list, 0, sizeof(*list));
}

int torrent_list_refresh(RpcClient *rpc, TorrentList *list, char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();
    cJSON_AddItemToObject(args, "fields",
                           field_array(SUMMARY_FIELDS, sizeof(SUMMARY_FIELDS) / sizeof(SUMMARY_FIELDS[0])));

    cJSON *out = NULL;
    if (rpc_call(rpc, "torrent-get", args, &out, err, errlen) != 0)
        return -1;

    cJSON *torrents = out ? cJSON_GetObjectItemCaseSensitive(out, "torrents") : NULL;
    if (!torrents || !cJSON_IsArray(torrents)) {
        snprintf(err, errlen, "unexpected response from torrent-get");
        cJSON_Delete(out);
        return -1;
    }

    /* Remember which torrent ids were selected so we can restore the flag
     * after repopulating the array (list order can shift between polls). */
    size_t old_selected_count = 0;
    for (size_t i = 0; i < list->count; i++)
        if (list->items[i].selected)
            old_selected_count++;
    int *old_selected_ids = NULL;
    if (old_selected_count) {
        old_selected_ids = malloc(old_selected_count * sizeof(int));
        size_t j = 0;
        for (size_t i = 0; i < list->count; i++)
            if (list->items[i].selected)
                old_selected_ids[j++] = list->items[i].id;
    }

    int count = cJSON_GetArraySize(torrents);
    if ((size_t)count > list->capacity) {
        size_t ncap = (size_t)count;
        Torrent *ni = realloc(list->items, ncap * sizeof(Torrent));
        if (!ni) {
            snprintf(err, errlen, "out of memory");
            free(old_selected_ids);
            cJSON_Delete(out);
            return -1;
        }
        list->items = ni;
        list->capacity = ncap;
    }

    for (int i = 0; i < count; i++)
        parse_summary(cJSON_GetArrayItem(torrents, i), &list->items[i]);
    list->count = (size_t)count;

    for (size_t s = 0; s < old_selected_count; s++) {
        for (size_t i = 0; i < list->count; i++) {
            if (list->items[i].id == old_selected_ids[s]) {
                list->items[i].selected = 1;
                break;
            }
        }
    }
    free(old_selected_ids);

    cJSON_Delete(out);
    return 0;
}

void torrent_detail_free(TorrentDetail *d)
{
    free(d->files);
    free(d->peers);
    free(d->trackers);
    memset(d, 0, sizeof(*d));
}

int torrent_get_detail(RpcClient *rpc, int id, TorrentDetail *out, char *err, size_t errlen)
{
    static const char *fields[] = {
        "id", "name", "hashString", "totalSize", "leftUntilDone", "percentDone",
        "status", "rateDownload", "rateUpload", "eta", "uploadRatio", "error",
        "errorString", "downloadDir", "peersConnected", "peersSendingToUs",
        "peersGettingFromUs", "dateAdded", "dateCreated", "doneDate",
        "downloadLimit", "downloadLimited", "uploadLimit", "uploadLimited",
        "files", "fileStats", "peers", "trackerStats",
    };

    cJSON *args = cJSON_CreateObject();
    int idarr[1] = {id};
    cJSON_AddItemToObject(args, "ids", id_array(idarr, 1));
    cJSON_AddItemToObject(args, "fields", field_array(fields, sizeof(fields) / sizeof(fields[0])));

    cJSON *resp = NULL;
    if (rpc_call(rpc, "torrent-get", args, &resp, err, errlen) != 0)
        return -1;

    cJSON *torrents = resp ? cJSON_GetObjectItemCaseSensitive(resp, "torrents") : NULL;
    if (!torrents || !cJSON_IsArray(torrents) || cJSON_GetArraySize(torrents) < 1) {
        snprintf(err, errlen, "torrent not found");
        cJSON_Delete(resp);
        return -1;
    }
    cJSON *t = cJSON_GetArrayItem(torrents, 0);

    memset(out, 0, sizeof(*out));
    parse_summary(t, &out->info);
    out->date_added = (long)get_i64(t, "dateAdded", 0);
    out->date_created = (long)get_i64(t, "dateCreated", 0);
    out->done_date = (long)get_i64(t, "doneDate", 0);
    out->download_limit = get_i64(t, "downloadLimit", 0);
    out->download_limited = get_int(t, "downloadLimited", 0);
    out->upload_limit = get_i64(t, "uploadLimit", 0);
    out->upload_limited = get_int(t, "uploadLimited", 0);

    cJSON *files = cJSON_GetObjectItemCaseSensitive(t, "files");
    cJSON *filestats = cJSON_GetObjectItemCaseSensitive(t, "fileStats");
    if (files && cJSON_IsArray(files)) {
        int n = cJSON_GetArraySize(files);
        out->files = calloc((size_t)n, sizeof(TorrentFile));
        out->file_count = (size_t)n;
        for (int i = 0; i < n; i++) {
            cJSON *f = cJSON_GetArrayItem(files, i);
            TorrentFile *tf = &out->files[i];
            get_str(f, "name", tf->name, sizeof(tf->name));
            tf->length = get_i64(f, "length", 0);
            tf->bytes_completed = get_i64(f, "bytesCompleted", 0);
            tf->wanted = 1;
            tf->priority = 0;
            if (filestats && cJSON_IsArray(filestats) && i < cJSON_GetArraySize(filestats)) {
                cJSON *fs = cJSON_GetArrayItem(filestats, i);
                tf->wanted = get_int(fs, "wanted", 1);
                tf->priority = get_int(fs, "priority", 0);
            }
        }
    }

    cJSON *peers = cJSON_GetObjectItemCaseSensitive(t, "peers");
    if (peers && cJSON_IsArray(peers)) {
        int n = cJSON_GetArraySize(peers);
        out->peers = calloc((size_t)n, sizeof(TorrentPeer));
        out->peer_count = (size_t)n;
        for (int i = 0; i < n; i++) {
            cJSON *p = cJSON_GetArrayItem(peers, i);
            TorrentPeer *tp = &out->peers[i];
            get_str(p, "address", tp->address, sizeof(tp->address));
            get_str(p, "clientName", tp->client_name, sizeof(tp->client_name));
            get_str(p, "flagStr", tp->flags, sizeof(tp->flags));
            tp->progress = get_dbl(p, "progress", 0.0);
            tp->rate_to_client = get_i64(p, "rateToClient", 0);
            tp->rate_to_peer = get_i64(p, "rateToPeer", 0);
        }
    }

    cJSON *trackers = cJSON_GetObjectItemCaseSensitive(t, "trackerStats");
    if (trackers && cJSON_IsArray(trackers)) {
        int n = cJSON_GetArraySize(trackers);
        out->trackers = calloc((size_t)n, sizeof(TorrentTracker));
        out->tracker_count = (size_t)n;
        for (int i = 0; i < n; i++) {
            cJSON *tr = cJSON_GetArrayItem(trackers, i);
            TorrentTracker *tt = &out->trackers[i];
            get_str(tr, "announce", tt->announce, sizeof(tt->announce));
            tt->tier = get_int(tr, "tier", 0);
            get_str(tr, "lastAnnounceResult", tt->last_announce_result, sizeof(tt->last_announce_result));
            tt->seeder_count = get_int(tr, "seederCount", -1);
            tt->leecher_count = get_int(tr, "leecherCount", -1);
        }
    }

    cJSON_Delete(resp);
    return 0;
}

int torrent_action(RpcClient *rpc, const char *method, const int *ids, size_t n, char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();
    cJSON_AddItemToObject(args, "ids", id_array(ids, n));
    return rpc_call(rpc, method, args, NULL, err, errlen);
}

int torrent_remove(RpcClient *rpc, const int *ids, size_t n, int delete_data, char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();
    cJSON_AddItemToObject(args, "ids", id_array(ids, n));
    cJSON_AddBoolToObject(args, "delete-local-data", delete_data);
    return rpc_call(rpc, "torrent-remove", args, NULL, err, errlen);
}

int torrent_add(RpcClient *rpc, const char *source, const char *download_dir, char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();

    if (str_starts_with(source, "magnet:") || str_starts_with(source, "http://") ||
        str_starts_with(source, "https://")) {
        cJSON_AddStringToObject(args, "filename", source);
    } else {
        size_t flen = 0;
        unsigned char *data = read_file_all(source, &flen);
        if (!data) {
            snprintf(err, errlen, "could not read file: %s", source);
            cJSON_Delete(args);
            return -1;
        }
        size_t b64size = base64_encoded_size(flen) + 1;
        char *b64 = malloc(b64size);
        if (!b64 || base64_encode(data, flen, b64, b64size) < 0) {
            snprintf(err, errlen, "could not base64-encode the file");
            free(data);
            free(b64);
            cJSON_Delete(args);
            return -1;
        }
        free(data);
        cJSON_AddStringToObject(args, "metainfo", b64);
        free(b64);
    }

    if (download_dir && download_dir[0])
        cJSON_AddStringToObject(args, "download-dir", download_dir);

    return rpc_call(rpc, "torrent-add", args, NULL, err, errlen);
}

int torrent_set_speed_limit(RpcClient *rpc, const int *ids, size_t n,
                             int64_t down_kbps, int down_enabled,
                             int64_t up_kbps, int up_enabled,
                             char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();
    cJSON_AddItemToObject(args, "ids", id_array(ids, n));
    cJSON_AddNumberToObject(args, "downloadLimit", (double)down_kbps);
    cJSON_AddBoolToObject(args, "downloadLimited", down_enabled);
    cJSON_AddNumberToObject(args, "uploadLimit", (double)up_kbps);
    cJSON_AddBoolToObject(args, "uploadLimited", up_enabled);
    return rpc_call(rpc, "torrent-set", args, NULL, err, errlen);
}

int torrent_set_file_wanted(RpcClient *rpc, int id, const int *file_indices, size_t n, int wanted,
                             char *err, size_t errlen)
{
    cJSON *args = cJSON_CreateObject();
    int idarr[1] = {id};
    cJSON_AddItemToObject(args, "ids", id_array(idarr, 1));
    cJSON_AddItemToObject(args, wanted ? "files-wanted" : "files-unwanted", id_array(file_indices, n));
    return rpc_call(rpc, "torrent-set", args, NULL, err, errlen);
}

int torrent_set_file_priority(RpcClient *rpc, int id, const int *file_indices, size_t n, int priority,
                               char *err, size_t errlen)
{
    const char *key = priority > 0 ? "priority-high" : (priority < 0 ? "priority-low" : "priority-normal");
    cJSON *args = cJSON_CreateObject();
    int idarr[1] = {id};
    cJSON_AddItemToObject(args, "ids", id_array(idarr, 1));
    cJSON_AddItemToObject(args, key, id_array(file_indices, n));
    return rpc_call(rpc, "torrent-set", args, NULL, err, errlen);
}