#include "config.h" #include #include #include #include #include #include Config g_cfg; static char g_cfg_path[512] = ""; static void set_defaults(void) { memset(&g_cfg, 0, sizeof(g_cfg)); g_cfg.port = 8787; strncpy(g_cfg.bind_iface, "wg0-mullvad", sizeof(g_cfg.bind_iface)-1); strncpy(g_cfg.iptv_api, "http://newlogin.freeopened.com/player_api.php", sizeof(g_cfg.iptv_api)-1); strncpy(g_cfg.dl_dir_tv, "/mnt/media/TV", sizeof(g_cfg.dl_dir_tv)-1); strncpy(g_cfg.dl_dir_mov, "/mnt/media/Movies", sizeof(g_cfg.dl_dir_mov)-1); strncpy(g_cfg.template_dir, "/usr/local/share/iptv-dl", sizeof(g_cfg.template_dir)-1); strncpy(g_cfg.data_dir, "/var/lib/iptv-dl", sizeof(g_cfg.data_dir)-1); g_cfg.max_recv_speed = 20L * 1024 * 1024; /* 20 MiB/s */ } /* minimal JSON string field parser — no full JSON lib dependency */ static void cfg_read_str(const char *json, const char *key, char *dst, size_t dsz) { char needle[128]; snprintf(needle, sizeof(needle), "\"%s\"", key); const char *p = strstr(json, needle); if (!p) return; p += strlen(needle); while (*p == ' ' || *p == ':') p++; if (*p != '"') return; p++; size_t i = 0; while (*p && *p != '"' && i < dsz-1) { if (*p == '\\' && *(p+1)) { p++; dst[i++] = *p++; } else dst[i++] = *p++; } dst[i] = 0; } static int cfg_read_int(const char *json, const char *key, int def) { char needle[128]; snprintf(needle, sizeof(needle), "\"%s\"", key); const char *p = strstr(json, needle); if (!p) return def; p += strlen(needle); while (*p == ' ' || *p == ':') p++; if (*p < '0' || *p > '9') return def; return atoi(p); } static long cfg_read_long(const char *json, const char *key, long def) { char needle[128]; snprintf(needle, sizeof(needle), "\"%s\"", key); const char *p = strstr(json, needle); if (!p) return def; p += strlen(needle); while (*p == ' ' || *p == ':') p++; if (*p < '0' || *p > '9') return def; return atol(p); } static int try_load(const char *path) { FILE *f = fopen(path, "r"); if (!f) return 0; fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); char *buf = malloc(sz + 1); if (!buf) { fclose(f); return 0; } fread(buf, 1, sz, f); buf[sz] = 0; fclose(f); cfg_read_str(buf, "bind_iface", g_cfg.bind_iface, sizeof(g_cfg.bind_iface)); cfg_read_str(buf, "iptv_api", g_cfg.iptv_api, sizeof(g_cfg.iptv_api)); cfg_read_str(buf, "iptv_user", g_cfg.iptv_user, sizeof(g_cfg.iptv_user)); cfg_read_str(buf, "iptv_pass", g_cfg.iptv_pass, sizeof(g_cfg.iptv_pass)); cfg_read_str(buf, "stream_base", g_cfg.stream_base, sizeof(g_cfg.stream_base)); cfg_read_str(buf, "dl_dir_tv", g_cfg.dl_dir_tv, sizeof(g_cfg.dl_dir_tv)); cfg_read_str(buf, "dl_dir_mov", g_cfg.dl_dir_mov, sizeof(g_cfg.dl_dir_mov)); cfg_read_str(buf, "discord_webhook", g_cfg.discord_webhook, sizeof(g_cfg.discord_webhook)); cfg_read_str(buf, "jellyfin_url", g_cfg.jellyfin_url, sizeof(g_cfg.jellyfin_url)); cfg_read_str(buf, "jellyfin_token", g_cfg.jellyfin_token, sizeof(g_cfg.jellyfin_token)); cfg_read_str(buf, "template_dir", g_cfg.template_dir, sizeof(g_cfg.template_dir)); cfg_read_str(buf, "data_dir", g_cfg.data_dir, sizeof(g_cfg.data_dir)); g_cfg.port = cfg_read_int(buf, "port", g_cfg.port); g_cfg.max_recv_speed = cfg_read_long(buf, "max_recv_speed", g_cfg.max_recv_speed); free(buf); strncpy(g_cfg_path, path, sizeof(g_cfg_path)-1); return 1; } const char *config_load(const char *explicit_path) { set_defaults(); /* 1. explicit argument */ if (explicit_path && *explicit_path) { if (try_load(explicit_path)) return g_cfg_path; fprintf(stderr, "iptv-dl: warning: cannot read config '%s', using defaults\n", explicit_path); return NULL; } /* 2. env var */ const char *env = getenv("IPTV_DL_CONFIG"); if (env && *env) { if (try_load(env)) return g_cfg_path; } /* 3. ~/.iptv-downloader/config.json */ char home_cfg[512]; const char *home = getenv("HOME"); if (!home) { struct passwd *pw = getpwuid(getuid()); if (pw) home = pw->pw_dir; } if (home) { snprintf(home_cfg, sizeof(home_cfg), "%s/.iptv-downloader/config.json", home); if (try_load(home_cfg)) return g_cfg_path; } /* 4. /etc/iptv-downloader/config.json */ if (try_load("/etc/iptv-downloader/config.json")) return g_cfg_path; /* 5. defaults only */ fprintf(stderr, "iptv-dl: no config file found, using built-in defaults\n"); return NULL; } void config_save(void) { if (!g_cfg_path[0]) { fprintf(stderr, "iptv-dl: no config path set, cannot save\n"); return; } FILE *f = fopen(g_cfg_path, "w"); if (!f) { perror("config_save"); return; } fprintf(f, "{\n"); fprintf(f, " \"port\": %d,\n", g_cfg.port); fprintf(f, " \"bind_iface\": \"%s\",\n", g_cfg.bind_iface); fprintf(f, " \"iptv_api\": \"%s\",\n", g_cfg.iptv_api); fprintf(f, " \"iptv_user\": \"%s\",\n", g_cfg.iptv_user); fprintf(f, " \"iptv_pass\": \"%s\",\n", g_cfg.iptv_pass); fprintf(f, " \"stream_base\": \"%s\",\n", g_cfg.stream_base); fprintf(f, " \"dl_dir_tv\": \"%s\",\n", g_cfg.dl_dir_tv); fprintf(f, " \"dl_dir_mov\": \"%s\",\n", g_cfg.dl_dir_mov); fprintf(f, " \"discord_webhook\": \"%s\",\n", g_cfg.discord_webhook); fprintf(f, " \"jellyfin_url\": \"%s\",\n", g_cfg.jellyfin_url); fprintf(f, " \"jellyfin_token\": \"%s\",\n", g_cfg.jellyfin_token); fprintf(f, " \"template_dir\": \"%s\",\n", g_cfg.template_dir); fprintf(f, " \"data_dir\": \"%s\",\n", g_cfg.data_dir); fprintf(f, " \"max_recv_speed\": %ld\n", g_cfg.max_recv_speed); fprintf(f, "}\n"); fclose(f); } void config_dump(void) { fprintf(stderr, "iptv-dl config:\n" " port=%d bind_iface=%s\n" " iptv_api=%s user=%s\n" " stream_base=%s\n" " dl_dir_tv=%s dl_dir_mov=%s\n" " template_dir=%s data_dir=%s\n" " max_recv_speed=%ld B/s\n", g_cfg.port, g_cfg.bind_iface, g_cfg.iptv_api, g_cfg.iptv_user, g_cfg.stream_base, g_cfg.dl_dir_tv, g_cfg.dl_dir_mov, g_cfg.template_dir, g_cfg.data_dir, g_cfg.max_recv_speed); } void config_history_path(char *buf, size_t n) { snprintf(buf, n, "%s/history.json", g_cfg.data_dir); } void config_notif_path(char *buf, size_t n) { snprintf(buf, n, "%s/notifications.json", g_cfg.data_dir); }