From f8af2245807de2ff86214b85a151143c33b5c4d7 Mon Sep 17 00:00:00 2001 From: rmuxnet Date: Tue, 9 Jun 2026 00:31:08 +0200 Subject: [PATCH] initial: modular iptv-dl with runtime config from ~/.iptv-downloader/config.json --- Makefile | 38 ++++ alioth-smoke | 52 +++++ buf.c | 28 +++ buf.h | 10 + buf.o | Bin 0 -> 2768 bytes config.c | 175 ++++++++++++++++ config.h | 63 ++++++ config.o | Bin 0 -> 11744 bytes discord.c | 25 +++ discord.h | 2 + discord.o | Bin 0 -> 2656 bytes footer.html | 1 + handlers.c | 454 ++++++++++++++++++++++++++++++++++++++++++ handlers.h | 24 +++ handlers.o | Bin 0 -> 37840 bytes header.html | 15 ++ http.c | 144 ++++++++++++++ http.h | 31 +++ http.o | Bin 0 -> 10192 bytes iptv-dl | Bin 0 -> 64376 bytes iptv.css | 41 ++++ iptv_api.c | 26 +++ iptv_api.h | 3 + iptv_api.o | Bin 0 -> 2688 bytes jellyfin.c | 74 +++++++ jellyfin.h | 3 + jellyfin.o | Bin 0 -> 5600 bytes json.c | 116 +++++++++++ json.h | 19 ++ json.o | Bin 0 -> 6368 bytes main.c | 80 ++++++++ main.o | Bin 0 -> 5272 bytes notify.c | 97 +++++++++ notify.h | 19 ++ notify.o | Bin 0 -> 7232 bytes queue.c | 301 ++++++++++++++++++++++++++++ queue.h | 56 ++++++ queue.o | Bin 0 -> 17984 bytes runit.run | 2 + server.c | 44 ++++ server.h | 2 + server.o | Bin 0 -> 4792 bytes static/downloads.js | 49 +++++ static/footer.html | 3 + static/header.html | 17 ++ static/iptv.css | 41 ++++ static/iptv.js | 20 ++ static/series_show.js | 65 ++++++ 48 files changed, 2140 insertions(+) create mode 100644 Makefile create mode 100755 alioth-smoke create mode 100644 buf.c create mode 100644 buf.h create mode 100644 buf.o create mode 100644 config.c create mode 100644 config.h create mode 100644 config.o create mode 100644 discord.c create mode 100644 discord.h create mode 100644 discord.o create mode 100644 footer.html create mode 100644 handlers.c create mode 100644 handlers.h create mode 100644 handlers.o create mode 100644 header.html create mode 100644 http.c create mode 100644 http.h create mode 100644 http.o create mode 100755 iptv-dl create mode 100644 iptv.css create mode 100644 iptv_api.c create mode 100644 iptv_api.h create mode 100644 iptv_api.o create mode 100644 jellyfin.c create mode 100644 jellyfin.h create mode 100644 jellyfin.o create mode 100644 json.c create mode 100644 json.h create mode 100644 json.o create mode 100644 main.c create mode 100644 main.o create mode 100644 notify.c create mode 100644 notify.h create mode 100644 notify.o create mode 100644 queue.c create mode 100644 queue.h create mode 100644 queue.o create mode 100755 runit.run create mode 100644 server.c create mode 100644 server.h create mode 100644 server.o create mode 100644 static/downloads.js create mode 100644 static/footer.html create mode 100644 static/header.html create mode 100644 static/iptv.css create mode 100644 static/iptv.js create mode 100644 static/series_show.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf8bf8a --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +CC = gcc +CFLAGS = -O2 -Wall -Wextra -I. -pthread +LDFLAGS = -pthread -lcurl +TARGET = iptv-dl +SRCS = main.c config.c buf.c json.c http.c discord.c jellyfin.c \ + iptv_api.c queue.c notify.c handlers.c server.c +OBJS = $(SRCS:.c=.o) + +# Install paths (override with: make install PREFIX=/usr/local) +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin +SHAREDIR = $(PREFIX)/share/iptv-dl +SYSCONFDIR = /etc/iptv-downloader + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CC) $(OBJS) $(LDFLAGS) -o $@ + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +install: $(TARGET) + install -d $(BINDIR) $(SHAREDIR) $(SYSCONFDIR) + install -m 755 $(TARGET) $(BINDIR)/iptv-dl + install -m 644 static/iptv.css $(SHAREDIR)/iptv.css + install -m 644 static/iptv.js $(SHAREDIR)/iptv.js + install -m 644 static/downloads.js $(SHAREDIR)/downloads.js + install -m 644 static/series_show.js $(SHAREDIR)/series_show.js + install -m 644 static/header.html $(SHAREDIR)/header.html + install -m 644 static/footer.html $(SHAREDIR)/footer.html + @echo "Installed. Create config at $(SYSCONFDIR)/config.json or ~/.iptv-downloader/config.json" + @echo "Run: $(BINDIR)/iptv-dl --dump-config (to see defaults)" + +clean: + rm -f $(OBJS) $(TARGET) + +.PHONY: all install clean diff --git a/alioth-smoke b/alioth-smoke new file mode 100755 index 0000000..2bff62a --- /dev/null +++ b/alioth-smoke @@ -0,0 +1,52 @@ +#!/bin/sh +# alioth smoke tests — exits 1 if any check fails +FAIL=0 + +ok() { printf " PASS %s\n" "$1"; } +fail() { printf " FAIL %s\n" "$1"; FAIL=1; } + +check() { + name="$1"; url="$2"; want="${3:-200}" + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url") + if [ "$code" = "$want" ] || { [ "$want" = "2xx" ] && [ "${code%?}" = "20" ]; }; then + ok "$name ($code)" + else + fail "$name — got $code, want $want ($url)" + fi +} + +echo "=== dashboard ===" +check "GET /" http://127.0.0.1:9090/ +check "GET /style.css" http://127.0.0.1:9090/style.css +check "GET /app.js" http://127.0.0.1:9090/app.js +check "GET /api/stats" http://127.0.0.1:9090/api/stats + +echo "=== iptv-dl ===" +check "GET /" http://127.0.0.1:8787/ +check "GET /iptv.css" http://127.0.0.1:8787/iptv.css +check "GET /downloads" http://127.0.0.1:8787/downloads +check "GET /api/downloads" http://127.0.0.1:8787/api/downloads +check "GET /movies" http://127.0.0.1:8787/movies +check "GET /series" http://127.0.0.1:8787/series + +echo "=== arr stack ===" +check "jellyfin" http://127.0.0.1:8096/jellyfin/health +check "sonarr" http://127.0.0.1:8989/sonarr/ping +check "radarr" http://127.0.0.1:7878/radarr/ping +check "prowlarr" http://127.0.0.1:9696/prowlarr/ping +check "qbit" http://127.0.0.1:8080/ + +echo "=== nginx proxy ===" +check "nginx /" http://127.0.0.1/ +check "nginx /style.css" http://127.0.0.1/style.css +check "nginx /app.js" http://127.0.0.1/app.js +check "nginx /iptv/" http://127.0.0.1/iptv/ +check "nginx /iptv/iptv.css" http://127.0.0.1/iptv/iptv.css + +echo "" +if [ "$FAIL" -eq 0 ]; then + echo "ALL PASS" +else + echo "SOME FAILED" +fi +exit $FAIL diff --git a/buf.c b/buf.c new file mode 100644 index 0000000..39deaac --- /dev/null +++ b/buf.c @@ -0,0 +1,28 @@ +#include "buf.h" +#include +#include +#include +#include + +void buf_init(Buf *b) { + b->data = malloc(4096); b->len = 0; b->cap = 4096; b->data[0] = 0; +} + +void buf_free(Buf *b) { + free(b->data); b->data = NULL; b->len = 0; b->cap = 0; +} + +void buf_append(Buf *b, const char *s, size_t n) { + while (b->len + n + 1 > b->cap) { b->cap *= 2; b->data = realloc(b->data, b->cap); } + memcpy(b->data + b->len, s, n); + b->len += n; + b->data[b->len] = 0; +} + +void buf_str(Buf *b, const char *s) { buf_append(b, s, strlen(s)); } + +void buf_fmt(Buf *b, const char *fmt, ...) { + char tmp[4096]; va_list ap; va_start(ap, fmt); + vsnprintf(tmp, sizeof(tmp), fmt, ap); va_end(ap); + buf_str(b, tmp); +} diff --git a/buf.h b/buf.h new file mode 100644 index 0000000..8d2aaad --- /dev/null +++ b/buf.h @@ -0,0 +1,10 @@ +#pragma once +#include + +typedef struct { char *data; size_t len; size_t cap; } Buf; + +void buf_init(Buf *b); +void buf_free(Buf *b); +void buf_append(Buf *b, const char *s, size_t n); +void buf_str(Buf *b, const char *s); +void buf_fmt(Buf *b, const char *fmt, ...) __attribute__((format(printf,2,3))); diff --git a/buf.o b/buf.o new file mode 100644 index 0000000000000000000000000000000000000000..b8137729051e46355584e08f49bd1e271cc5f73e GIT binary patch literal 2768 zcmb<-^>JfjWMqH=Mg}_u1P><4z!1QWU^{@B4h(z@ybQq}oxk@p2rw|b012k?=d*%n zk51-e3=I5&&K%K)`Q;hX()9S{TR;+E^&Z{!P}T1bcpPU2iNlpThI)4Xbqw+B{1WQX zc{SLhH;U1t`3;9hw>yVNce#K^x4lQN2(yRfwPH9u*G(1_qzb7L@=n-J=pAz`)RQpu~Y+vqdFAfPsNuvqz->Oixj10Mm0+ zCV=TBDht5$8kG%TdW*^dFuh0Rf&c?Uibpq#ss;ndvKAE$0R{%c10J29JbHapIQD^J z!K1fDF(@orJ&*N7pkdXXr^bVXQ*IgU}R=sVhpmB zfq{jAfkA3ujfW%;FjuFb|1ewLaz#syp8LNUA7%K!ArFqynCNMHE zNH8!k$UxSU9|r?O&H^OHz`!62qQP>}U^x>828Ixj7y|=CEr@pF6X<1f z<&)@PcHvWKV{zfrXlC``GhmA6({SWdaO9J4;uCP<;{e&!15(exz@P=9!Df2#DFpKg zxbtx^s4y@vtO1ELFfjZB(NOshicD{~_yipJI2^e_!niQV|11n2|6`M9#*!l;*%1_q zs5*EU7#M_*)Puu@fq_AkfdQ00P!&VN5TqUwJJ9fg#t|$^ku|A8%@2SIg2Rx3fdQl+ zn|e?@U=z28nsWha4meC17#Q4fxYG}+9_BT07&9<1gyB%1fJ2-isWeS5nL*FT*#N|h z&&pjTW{1fervtfJJMM7@&KiV_CBl*E!m2EC->Vg|jUd=N)3 zxwyp8j6p9sKQ}iuuY^G_FTW&J&)qLn7aX5ZetKT1UO`cQL26M+C4(N=ywr^Nw4%h^ zRH%C>r9j~Z3Q{&SgCKP-1H(U%B$6C@-jasKDNH>~X$C0KF)%QEfC_*LBal&`xC9A- z%2N=n#=rm$N>Ew^@nP5w6j?A15)Co~iOZnFz`y`9lU)6VQ2Sx|0%o=Y)P8jT!uT*6 zRIY&B1WQ*i{Q*$@pmH202BJY~k!cU8{U9-9oCDR5ECyo3^n=(S3`)nK{EDu;04jj) zE>Kv3^ucfe)PAUPm@vZvr~oYe!z9r4XXA+f2xx+;fNBKAKdAhG>4(KX$Uji4pei9$ z0W?AzKnfTb7(itMsBA*l4>A*~6rvGIegG+EU|=YO@?jLzI2e~f5Lynx1)z)zQ1@3s J`7jDyKL9WSbQ%Bv literal 0 HcmV?d00001 diff --git a/config.c b/config.c new file mode 100644 index 0000000..0ed9fac --- /dev/null +++ b/config.c @@ -0,0 +1,175 @@ +#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); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..0eff778 --- /dev/null +++ b/config.h @@ -0,0 +1,63 @@ +#pragma once +#include + +/* ── Compile-time limits (not user-configurable) ───────────────── */ +#define MAX_DL 256 +#define MAX_NOTIF 50 +#define BACKLOG 32 +#define CURL_DL_TIMEOUT 7200L +#define CURL_API_TIMEOUT 20L + +/* ── Runtime configuration struct ─────────────────────────────── */ +typedef struct { + /* Network */ + int port; + char bind_iface[64]; /* "" = no binding, "wg0-mullvad" = VPN only */ + + /* IPTV provider (Xtream Codes) */ + char iptv_api[512]; + char iptv_user[128]; + char iptv_pass[128]; + char stream_base[512]; + + /* Download directories */ + char dl_dir_tv[512]; + char dl_dir_mov[512]; + + /* Integrations */ + char discord_webhook[512]; + char jellyfin_url[512]; + char jellyfin_token[256]; + + /* Paths */ + char template_dir[512]; /* dir containing header.html, footer.html, iptv.css, *.js */ + char data_dir[512]; /* dir for history.json, notifications.json */ + + /* Limits */ + long max_recv_speed; /* bytes/sec, 0 = unlimited */ +} Config; + +extern Config g_cfg; + +/* + * Config load order (first found wins): + * 1. Path passed to config_load(path) if non-NULL + * 2. $IPTV_DL_CONFIG env var + * 3. ~/.iptv-downloader/config.json + * 4. /etc/iptv-downloader/config.json + * 5. Built-in defaults (works with no config file) + * + * Returns path used (static buffer), or NULL if using defaults only. + */ +const char *config_load(const char *explicit_path); + +/* Write current g_cfg back to the path that was loaded */ +void config_save(void); + +/* Print current config as JSON to stderr */ +void config_dump(void); + +/* Return history file path: data_dir/history.json */ +void config_history_path(char *buf, size_t n); +/* Return notifications file path: data_dir/notifications.json */ +void config_notif_path(char *buf, size_t n); diff --git a/config.o b/config.o new file mode 100644 index 0000000000000000000000000000000000000000..2d99f51bf64d26d024d0bb4ca774833d51bed57e GIT binary patch literal 11744 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 +#include +#include + +void discord_notify(const char *msg) { + if (!g_cfg.discord_webhook[0]) return; + CURL *c = curl_easy_init(); + if (!c) return; + char payload[1024]; + snprintf(payload, sizeof(payload), + "{\"username\":\"IPTV Downloader\"," + "\"avatar_url\":\"https://i.imgur.com/ryNVNoI.png\"," + "\"content\":\"%s\"}", msg); + struct curl_slist *hdrs = curl_slist_append(NULL, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, g_cfg.discord_webhook); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, payload); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); + curl_easy_perform(c); + curl_slist_free_all(hdrs); + curl_easy_cleanup(c); +} diff --git a/discord.h b/discord.h new file mode 100644 index 0000000..af83d11 --- /dev/null +++ b/discord.h @@ -0,0 +1,2 @@ +#pragma once +void discord_notify(const char *msg); diff --git a/discord.o b/discord.o new file mode 100644 index 0000000000000000000000000000000000000000..7cbcc316cdbc19900778b123398d89bd39894a68 GIT binary patch literal 2656 zcmb<-^>JfjWMqH=Mg}_u1P><4!0><#!FB*M9T@l+co{qz-&n9PFr;{Nv#4q?Ffe#@ zwx~$3FfbS%Xs`tOAh6ts2XhYQSw=y#_FnBZ`0qH!P#xL&z(lGbG4?9qCFzvUFj)NUUY4v$_@koZ0Zu<;(fH7Wu=om*54SQr=}8hTAZ@`nE*yw3ak zgwz=rpt?Lej~O0-NqF>zs0bJy*e97@jk*RuwtLi?_k=a^FD;`hIq!O8{)i|KmYyz5A`Qh>gBV4|Nnb*Le(LA1LOw= zhH9nK;?$zN#N1RRD<#i>kT3<8{PMh<{KS;hA|)NA#InSa#G?4pq8ueFrHqo2f?_Lu z{Y<^g-1O2Sz2y8{{h~_0Fu#0Hy@I@SB^{;Y{JfIXyb>iVCDmf3T96IS5E0#w%7RoY zg~Wn_oXq6JlFa-({jB2rJO&1LXJ;z~4R^m#O$9?UJwrW11tS9^GXoQ21CVY8P#iEQ zFfcH%GB7Z>`-L(vF)$urVPJr#9Y!?12t*qLV^t6XV}$^tG!Hw+1V#o19R>yl8K_(k zDB-#D9pI?q;^T1V_HAY^>|*TU|@jgL8t|>v~Y-9;t+SjAs&K5 zJQas{IS%n|9O83vh%=;Q7ANNyrRXIy==nGsfCxjFSbSc7NoHClLwbC2S~^2=X;Dsm zYGQF^d}dx|2}5ySK~ZL2Ng71FI485XBtEgAAT=)qA_7)foLZ7!P=X{-kXn?MUz7_G zfoMuAN==PV%*la>fwd*)q$cK-7BIxe7ndX^XU8XJWXGo^X67KJAy5JV6;<|}pjcpF z2!l}I^vce_!0_ik1mr=*LF$>2#M`0bptQ_^B;E%V2dM|i!OWQo6$godtb~a#fr^9F zgWTf)l4M|DSO*mcsRxxWF!hIUh+n}W4lZXHAo&p4oS!(rFsQL z`30#(C6x?%U;|S#;?s%}b3qvd>;TFLkiS4)2E`$=Nsu~_fdN(yz~s=&0%>U2!qmgm zod87y0|P??$Z!S*22h-Un4mNVQX>H(7#JATpk*p3jY5UM)FhB%FbkDH7Y3EDAT!C; zZwR&D2-O;f8BmMr(ZoS^frOya3?P4l*wAVNBn8Ie&_n_&`@w=xyFhG^9uVz`BnINc zurX9Wj1Qw>`e6J>1_lODzDHMX0Tn=Z7tH-I{e@8bp~_*x3>i=X54Z?~fvz7^Hi4oV z6b=v-5YhvhpkVO~ihocM0n-l?2l)qT6+|PH+yE8K04ZQ#U;yP^P;G{;A7mza+=Gk& lwb4K{tULm-K^STrhzZ7m;QABHL?IHO?yp7>Mb!T=VF1&*m8<{& literal 0 HcmV?d00001 diff --git a/footer.html b/footer.html new file mode 100644 index 0000000..a1f0ee4 --- /dev/null +++ b/footer.html @@ -0,0 +1 @@ + diff --git a/handlers.c b/handlers.c new file mode 100644 index 0000000..587fa32 --- /dev/null +++ b/handlers.c @@ -0,0 +1,454 @@ +#include "handlers.h" +#include "config.h" +#include "buf.h" +#include "json.h" +#include "http.h" +#include "queue.h" +#include "notify.h" +#include "iptv_api.h" +#include +#include +#include +#include +#include +#include +#include + +/* ── safe_title: strip path-unsafe chars from a title ─────────── */ +static void safe_title(char *dst, size_t dsz, const char *src) { + size_t i = 0; + for (const char *p = src; *p && i < dsz-1; p++) { + if ((*p>='A'&&*p<='Z')||(*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')|| + *p==' '||*p=='-'||*p=='_'||*p=='('||*p==')') + dst[i++] = *p; + } + dst[i] = 0; +} + +/* ── Page handlers ─────────────────────────────────────────────── */ + +void handle_index(int fd) { + Buf b; buf_init(&b); + buf_str(&b, "

iptv downloader

" + "

" + "Browse series, " + "movies, or " + "search by name." + "

"); + send_page(fd, "IPTV Downloader", &b, NULL); + buf_free(&b); +} + +void handle_series_cats(int fd) { + char *json = api_get("get_series_categories", ""); + int n; char **arr = json_array(json, &n); + Buf b; buf_init(&b); + buf_str(&b, "

series categories

" + "" + "
"); + for (int i = 0; i < n; i++) { + char *id = json_str(arr[i],"category_id"), *name = json_str(arr[i],"category_name"); + buf_fmt(&b, "
", id?id:""); + html_esc(&b, name?name:"?"); buf_str(&b, "
"); + free(id); free(name); free(arr[i]); + } + buf_str(&b, "
"); free(arr); free(json); + send_page(fd, "Series", &b, NULL); + buf_free(&b); +} + +void handle_series_list(int fd, const char *qs) { + char *cat_id = qparam(qs, "id"); + char extra[128]; snprintf(extra, sizeof(extra), "&category_id=%s", cat_id?cat_id:""); + char *json = api_get("get_series", extra); + int n; char **arr = json_array(json, &n); + Buf b; buf_init(&b); + buf_fmt(&b, "

series / category

" + "

series — %d results

", n); + buf_str(&b, "
"); + for (int i = 0; i < n; i++) { + char *sid = json_str(arr[i],"series_id"), *name = json_str(arr[i],"name"), *cover = json_str(arr[i],"cover"); + buf_fmt(&b, "", sid?sid:""); + if (cover && cover[0]) { + buf_str(&b,"
"); + } + buf_str(&b,"
"); html_esc(&b, name?name:"?"); buf_str(&b,"
"); + free(sid); free(name); free(cover); free(arr[i]); + } + buf_str(&b, "
"); free(cat_id); free(arr); free(json); + send_page(fd, "Series", &b, NULL); + buf_free(&b); +} + +void handle_series_show(int fd, const char *qs) { + char *sid = qparam(qs, "id"); + char extra[64]; snprintf(extra, sizeof(extra), "&series_id=%s", sid?sid:""); + char *json = api_get("get_series_info", extra); + const char *info_sec = strstr(json, "\"info\":"); + char *title_raw = json_str(info_sec ? info_sec : json, "name"); + char *cover_raw = json_str(info_sec ? info_sec : json, "cover"); + const char *title = title_raw ? title_raw : "Unknown"; + const char *eps_start = strstr(json, "\"episodes\":"); + + Buf b; buf_init(&b); + buf_str(&b, "

series / "); html_esc(&b, title); + buf_str(&b, "

"); html_esc(&b, title); buf_str(&b, "

"); + buf_str(&b, "
"); + buf_str(&b, "
" + "" + "
"); + buf_str(&b, "
"); + + /* Inject TITLE, COVER, SEASONS as window vars before the external script */ + Buf inl; buf_init(&inl); + buf_str(&inl, ""); + buf_append(&b, inl.data, inl.len); + buf_free(&inl); + + free(sid); free(title_raw); free(cover_raw); free(json); + send_page(fd, "Series", &b, "/series_show.js"); + buf_free(&b); +} + +void handle_movie_cats(int fd) { + char *json = api_get("get_vod_categories", ""); + int n; char **arr = json_array(json, &n); + Buf b; buf_init(&b); + buf_str(&b, "

movie categories

" + "" + "
"); + for (int i = 0; i < n; i++) { + char *id = json_str(arr[i],"category_id"), *name = json_str(arr[i],"category_name"); + buf_fmt(&b, "
", id?id:""); + html_esc(&b, name?name:"?"); buf_str(&b, "
"); + free(id); free(name); free(arr[i]); + } + buf_str(&b, "
"); free(arr); free(json); + send_page(fd, "Movies", &b, NULL); + buf_free(&b); +} + +void handle_movie_list(int fd, const char *qs) { + char *cat_id = qparam(qs, "id"); + char extra[128]; snprintf(extra, sizeof(extra), "&category_id=%s", cat_id?cat_id:""); + char *json = api_get("get_vod_streams", extra); + int n; char **arr = json_array(json, &n); + Buf b; buf_init(&b); + buf_fmt(&b, "

movies / category

" + "

movies — %d results

", n); + buf_str(&b, "
"); + for (int i = 0; i < n; i++) { + char *vid = json_str(arr[i],"stream_id"), *name = json_str(arr[i],"name"); + char *ext = json_str(arr[i],"container_extension"), *icon = json_str(arr[i],"stream_icon"); + buf_fmt(&b, ""); + if (icon && icon[0]) { + buf_str(&b,"
"); + } + buf_str(&b, "
"); html_esc(&b, name?name:"?"); buf_str(&b, "
"); + free(vid); free(name); free(ext); free(icon); free(arr[i]); + } + buf_str(&b, "
"); free(cat_id); free(arr); free(json); + send_page(fd, "Movies", &b, NULL); + buf_free(&b); +} + +void handle_search(int fd, const char *qs) { + char *q = qparam(qs, "q"), *type = qparam(qs, "type"); + int is_series = !type || strcmp(type, "movies") != 0; + Buf b; buf_init(&b); + buf_str(&b, "

search

"); + buf_str(&b, "" + "", + is_series?" selected":"", !is_series?" selected":""); + buf_str(&b, "
"); + if (!q || !q[0]) { + send_page(fd, "Search", &b, NULL); + buf_free(&b); free(q); free(type); return; + } + char *json = is_series ? api_get("get_series","") : api_get("get_vod_streams",""); + int n; char **arr = json_array(json, &n); int count = 0; + buf_str(&b, "
"); + for (int i = 0; i < n; i++) { + char *name = json_str(arr[i], "name"); + if (!str_icontains(name, q)) { free(name); free(arr[i]); continue; } + if (is_series) { + char *sid = json_str(arr[i],"series_id"), *cover = json_str(arr[i],"cover"); + buf_fmt(&b, "", sid?sid:""); + if (cover && cover[0]) { buf_str(&b,"
"); } + buf_str(&b,"
"); html_esc(&b,name); buf_str(&b,"
"); + free(sid); free(cover); + } else { + char *vid = json_str(arr[i],"stream_id"), *ext = json_str(arr[i],"container_extension"), *icon = json_str(arr[i],"stream_icon"); + buf_fmt(&b,""); + if (icon&&icon[0]){buf_str(&b,"
");} + buf_str(&b,"
"); html_esc(&b,name); buf_str(&b,"
"); + free(vid); free(ext); free(icon); + } + free(name); free(arr[i]); count++; + } + buf_str(&b, "
"); free(arr); free(json); + char cnt[64]; snprintf(cnt,sizeof(cnt),"

%d results

",count); + Buf out; buf_init(&out); + const char *gp = strstr(b.data,"
downloads

" + "
" + "" + "" + "" + "" + "
" + "

loading...

"); + send_page(fd, "Downloads", &b, "/downloads.js"); + buf_free(&b); +} + +/* ── API handlers ──────────────────────────────────────────────── */ + +void handle_api_downloads(int fd) { + Buf b; buf_init(&b); + buf_str(&b, "["); + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + Download *d = &g_downloads[i]; + int pct = d->total > 0 ? (int)(d->downloaded*100/d->total) : 0; + long speed = (long)d->speed_bps; + int eta = -1; + if (d->speed_bps > 0 && d->total > d->downloaded) + eta = (int)((d->total - d->downloaded) / d->speed_bps); + if (i > 0) buf_str(&b, ","); + buf_fmt(&b, "{\"id\":\"%s\",\"name\":\"%s\",\"status\":\"%s\"," + "\"downloaded\":%ld,\"total\":%ld,\"pct\":%d," + "\"speed_bps\":%ld,\"eta_s\":%d,\"cover_url\":\"%s\"}", + d->id, d->name, d->status, d->downloaded, d->total, + pct, speed, eta, d->cover_url); + } + pthread_mutex_unlock(&g_dl_mutex); + buf_str(&b, "]"); + send_json_buf(fd, &b); buf_free(&b); +} + +void handle_api_cancel(int fd, const char *body) { + char *id = json_str(body, "id"); + if (id) { + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, id) == 0) { + g_downloads[i].cancelled = 1; + if (strcmp(g_downloads[i].status, "queued") == 0) + strncpy(g_downloads[i].status, "cancelled", 63); + break; + } + } + pthread_mutex_unlock(&g_dl_mutex); + pthread_mutex_lock(&g_q_mutex); + QNode *prev = NULL, *node = g_q_head; + while (node) { + if (strcmp(node->task->id, id) == 0) { + if (prev) prev->next = node->next; else g_q_head = node->next; + if (g_q_tail == node) g_q_tail = prev; + free(node->task); free(node); break; + } + prev = node; node = node->next; + } + pthread_mutex_unlock(&g_q_mutex); + history_save(); + free(id); + } + send_json(fd, "{\"ok\":1}"); +} + +void handle_api_clear(int fd) { + pthread_mutex_lock(&g_dl_mutex); + int nc = 0; + for (int i = 0; i < g_dl_count; i++) { + const char *s = g_downloads[i].status; + if (strcmp(s,"done")!=0 && strncmp(s,"error",5)!=0 && + strcmp(s,"cancelled")!=0 && strcmp(s,"interrupted")!=0) { + if (nc != i) g_downloads[nc] = g_downloads[i]; + nc++; + } + } + g_dl_count = nc; + pthread_mutex_unlock(&g_dl_mutex); + history_save(); + send_json(fd, "{\"ok\":1}"); +} + +void handle_api_clear_cancelled(int fd) { + pthread_mutex_lock(&g_dl_mutex); + int nc = 0; + for (int i = 0; i < g_dl_count; i++) { + const char *s = g_downloads[i].status; + if (strcmp(s,"cancelled")!=0 && strcmp(s,"interrupted")!=0) { + if (nc != i) g_downloads[nc] = g_downloads[i]; + nc++; + } + } + g_dl_count = nc; + pthread_mutex_unlock(&g_dl_mutex); + history_save(); + send_json(fd, "{\"ok\":1}"); +} + +void handle_api_clean_partials(int fd) { + clean_partials(); + send_json(fd, "{\"ok\":1}"); +} + +void handle_api_retry_interrupted(int fd) { + pthread_mutex_lock(&g_dl_mutex); + int n = g_dl_count; + DlTask *tasks[MAX_DL]; int tc = 0; + for (int i = 0; i < n; i++) { + Download *d = &g_downloads[i]; + if (strcmp(d->status, "interrupted") != 0) continue; + DlTask *t = calloc(1, sizeof(DlTask)); + if (d->url[0]) { + strncpy(t->url, d->url, sizeof(t->url)-1); + } else { + snprintf(t->url, sizeof(t->url), "%s/series/%s/%s/%s", + g_cfg.stream_base, g_cfg.iptv_user, g_cfg.iptv_pass, d->id); + } + if (d->dest_dir[0]) { + strncpy(t->dest_dir, d->dest_dir, sizeof(t->dest_dir)-1); + } else { + char title[256]; strncpy(title, d->name, sizeof(title)-1); + int season = 0; + for (char *p = title; *p; p++) { + if (*p==' ' && *(p+1)=='S' && isdigit((unsigned char)*(p+2)) && isdigit((unsigned char)*(p+3))) { + season = atoi(p+2); *p = 0; + } + } + char safe[256]; safe_title(safe, sizeof(safe), title); + snprintf(t->dest_dir, sizeof(t->dest_dir), "%s/%s/Season %02d", g_cfg.dl_dir_tv, safe, season); + } + if (d->filename[0]) { + strncpy(t->filename, d->filename, sizeof(t->filename)-1); + } else { + const char *dot = strrchr(d->id, '.'); const char *ext = dot ? dot+1 : "mp4"; + snprintf(t->filename, sizeof(t->filename), "%s.%s", d->name, ext); + } + strncpy(t->name, d->name, sizeof(t->name)-1); + strncpy(t->id, d->id, sizeof(t->id)-1); + strncpy(t->cover_url,d->cover_url,sizeof(t->cover_url)-1); + strncpy(d->status, "queued", 63); + d->downloaded = 0; d->total = 0; d->speed_bps = 0; d->cancelled = 0; + tasks[tc++] = t; + } + pthread_mutex_unlock(&g_dl_mutex); + history_save(); + for (int i = 0; i < tc; i++) { + QNode *node = malloc(sizeof(QNode)); + node->task = tasks[i]; node->next = NULL; + pthread_mutex_lock(&g_q_mutex); + if (g_q_tail) g_q_tail->next = node; else g_q_head = node; + g_q_tail = node; + pthread_cond_signal(&g_q_cond); + pthread_mutex_unlock(&g_q_mutex); + } + char resp[32]; snprintf(resp, sizeof(resp), "{\"retried\":%d}", tc); + send_json(fd, resp); +} + +void handle_api_download(int fd, const char *body) { + char *title_raw = json_str(body,"series_title"), *season_raw = json_str(body,"season"); + char *cover = json_str(body,"cover_url"); + const char *title = title_raw?title_raw:"Unknown", *season = season_raw?season_raw:"01"; + char safe[256]; safe_title(safe, sizeof(safe), title); + char dest[512]; snprintf(dest, sizeof(dest), "%s/%s/Season %02d", g_cfg.dl_dir_tv, safe, atoi(season)); + const char *eps_json = strstr(body, "\"episodes\":"); + if (!eps_json) { send_json(fd,"{\"queued\":0}"); free(title_raw);free(season_raw);free(cover); return; } + int n; char **arr = json_array(eps_json + strlen("\"episodes\":"), &n); + int queued = 0; + for (int i = 0; i < n; i++) { + char *eid = json_str(arr[i],"id"), *epn = json_str(arr[i],"episode_num"), *ext = json_str(arr[i],"ext"); + const char *ex = (ext && *ext) ? ext : "mp4"; + if (eid) { + char fname[256], name[256]; + snprintf(fname, sizeof(fname), "%s S%02dE%s.%s", safe, atoi(season), epn?epn:"??", ex); + snprintf(name, sizeof(name), "%s S%02dE%s", title, atoi(season), epn?epn:"??"); + char sf[128]; snprintf(sf, sizeof(sf), "%s.%s", eid, ex); + queue_download(sf, fname, dest, name, 0, cover); queued++; + } + free(eid); free(epn); free(ext); free(arr[i]); + } + free(arr); free(title_raw); free(season_raw); free(cover); + char resp[64]; snprintf(resp, sizeof(resp), "{\"queued\":%d}", queued); + send_json(fd, resp); +} + +void handle_api_download_movie(int fd, const char *body) { + char *id = json_str(body,"id"), *title = json_str(body,"title"); + char *ext = json_str(body,"ext"), *cover = json_str(body,"cover_url"); + if (!ext || !*ext) { free(ext); ext = strdup("mp4"); } + const char *t = title ? title : "Movie"; + char safe[256]; safe_title(safe, sizeof(safe), t); + char dest[512]; snprintf(dest, sizeof(dest), "%s/%s", g_cfg.dl_dir_mov, safe); + char fname[256]; snprintf(fname, sizeof(fname), "%s.%s", safe, ext); + char sf[128]; snprintf(sf, sizeof(sf), "%s.%s", id?id:"0", ext); + queue_download(sf, fname, dest, safe, 1, cover); + free(id); free(title); free(ext); free(cover); + send_json(fd, "{\"queued\":1}"); +} + +void handle_api_notifications(int fd) { + Buf b; buf_init(&b); + buf_str(&b, "["); + int start = (g_notif_count < MAX_NOTIF) ? 0 : g_notif_tail; + for (int i = g_notif_count - 1; i >= 0; i--) { + Notification *n = &g_notifs[(start + i) % MAX_NOTIF]; + if (i < g_notif_count - 1) buf_str(&b, ","); + buf_fmt(&b, "{\"ts\":%ld,\"type\":\"%s\",\"msg\":", (long)n->ts, n->type); + buf_str(&b, "\""); + for (const char *p = n->msg; *p; p++) { + if (*p == '"') buf_str(&b, "\\\""); + else if (*p == '\\') buf_str(&b, "\\\\"); + else { char ch[2] = {*p, 0}; buf_str(&b, ch); } + } + buf_str(&b, "\"}"); + } + buf_str(&b, "]"); + send_json_buf(fd, &b); buf_free(&b); +} + +void handle_api_notifications_test(int fd) { + notify_add(":white_check_mark: Downloaded: **Test Episode S01E99**", "done"); + notify_add(":x: Download failed: **Test Episode S01E00** — connection timeout", "error"); + send_json(fd, "{\"ok\":1,\"added\":2}"); +} + +void handle_api_notifications_dismiss(int fd, const char *body) { + char *ts_s = json_str(body, "ts"); + if (!ts_s) { send_json(fd, "{\"ok\":0}"); return; } + time_t ts = (time_t)atol(ts_s); free(ts_s); + int found = notify_dismiss(ts); + send_json(fd, found ? "{\"ok\":1}" : "{\"ok\":0}"); +} diff --git a/handlers.h b/handlers.h new file mode 100644 index 0000000..bde760f --- /dev/null +++ b/handlers.h @@ -0,0 +1,24 @@ +#pragma once + +/* Page handlers */ +void handle_index(int fd); +void handle_series_cats(int fd); +void handle_series_list(int fd, const char *qs); +void handle_series_show(int fd, const char *qs); +void handle_movie_cats(int fd); +void handle_movie_list(int fd, const char *qs); +void handle_search(int fd, const char *qs); +void handle_downloads(int fd); + +/* API handlers */ +void handle_api_downloads(int fd); +void handle_api_cancel(int fd, const char *body); +void handle_api_clear(int fd); +void handle_api_clear_cancelled(int fd); +void handle_api_clean_partials(int fd); +void handle_api_retry_interrupted(int fd); +void handle_api_download(int fd, const char *body); +void handle_api_download_movie(int fd, const char *body); +void handle_api_notifications(int fd); +void handle_api_notifications_test(int fd); +void handle_api_notifications_dismiss(int fd, const char *body); diff --git a/handlers.o b/handlers.o new file mode 100644 index 0000000000000000000000000000000000000000..a154abdc5ba2d0d595388b2798503e8a26ccaa63 GIT binary patch literal 37840 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 literal 0 HcmV?d00001 diff --git a/header.html b/header.html new file mode 100644 index 0000000..73f9c62 --- /dev/null +++ b/header.html @@ -0,0 +1,15 @@ + + +{{TITLE}} — iptv + + + + + +

iptv

+
diff --git a/http.c b/http.c new file mode 100644 index 0000000..8a317cd --- /dev/null +++ b/http.c @@ -0,0 +1,144 @@ +#include "http.h" +#include "config.h" /* g_cfg */ +#include +#include +#include +#include +#include + +char *g_header = NULL; int g_header_len = 0; +char *g_footer = NULL; int g_footer_len = 0; +char *g_css = NULL; int g_css_len = 0; + +StaticJS g_js[] = { + { "/iptv.js", "iptv.js", NULL, 0 }, + { "/downloads.js", "downloads.js", NULL, 0 }, + { "/series_show.js", "series_show.js", NULL, 0 }, + { NULL, NULL, NULL, 0 } +}; + +static int load_file(const char *path, char **out, int *out_len) { + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "iptv-dl: cannot open %s: %s\n", path, strerror(errno)); return 0; } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + *out = malloc(sz + 1); + if (!*out) { fclose(f); return 0; } + *out_len = (int)fread(*out, 1, sz, f); + (*out)[*out_len] = '\0'; + fclose(f); + return 1; +} + +int http_load_templates(void) { + char path[512]; + snprintf(path, sizeof(path), "%s/header.html", g_cfg.template_dir); + if (!load_file(path, &g_header, &g_header_len)) return 0; + snprintf(path, sizeof(path), "%s/footer.html", g_cfg.template_dir); + if (!load_file(path, &g_footer, &g_footer_len)) return 0; + snprintf(path, sizeof(path), "%s/iptv.css", g_cfg.template_dir); + if (!load_file(path, &g_css, &g_css_len)) return 0; + for (int i = 0; g_js[i].url_path; i++) { + snprintf(path, sizeof(path), "%s/%s", g_cfg.template_dir, g_js[i].fs_name); + /* non-fatal if JS file missing */ + load_file(path, &g_js[i].data, &g_js[i].len); + } + fprintf(stderr, "iptv-dl: templates loaded (header=%d footer=%d css=%d bytes)\n", + g_header_len, g_footer_len, g_css_len); + return 1; +} + +int parse_request(int fd, Req *req) { + char buf[131072]; int total = 0, n; + memset(req, 0, sizeof(*req)); + while ((n = read(fd, buf+total, sizeof(buf)-1-total)) > 0) { + total += n; buf[total] = 0; + if (strstr(buf, "\r\n\r\n")) break; + } + if (total <= 0) return -1; + buf[total] = 0; + char *sp1 = strchr(buf, ' '); if (!sp1) return -1; + size_t mlen = sp1 - buf; if (mlen >= 8) mlen = 7; + memcpy(req->method, buf, mlen); req->method[mlen] = 0; + char *sp2 = strchr(sp1+1, ' '); if (!sp2) return -1; + size_t plen = sp2 - sp1 - 1; if (plen >= 256) plen = 255; + char full[256]; memcpy(full, sp1+1, plen); full[plen] = 0; + char *qm = strchr(full, '?'); + if (qm) { *qm = 0; strncpy(req->query, qm+1, sizeof(req->query)-1); } + strncpy(req->path, full, sizeof(req->path)-1); + char *body_start = strstr(buf, "\r\n\r\n"); + if (body_start) { + body_start += 4; + int blen = total - (int)(body_start - buf); + if (blen > 0 && blen < (int)sizeof(req->body)) { memcpy(req->body, body_start, blen); req->body_len = blen; } + } + return 0; +} + +void send_page(int fd, const char *title, Buf *body, const char *script_src) { + Buf page; buf_init(&page); + buf_str(&page, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n"); + const char *ph = strstr(g_header, "{{TITLE}}"); + if (ph) { + buf_append(&page, g_header, ph - g_header); + buf_str(&page, title); + buf_str(&page, ph + 9); + } else { + buf_str(&page, g_header); + } + buf_append(&page, body->data, body->len); + if (script_src && script_src[0]) + buf_fmt(&page, "", script_src); + buf_str(&page, g_footer); + write(fd, page.data, page.len); + buf_free(&page); +} + +void send_css(int fd) { + char hdr[256]; + int hl = snprintf(hdr, sizeof(hdr), + "HTTP/1.1 200 OK\r\nContent-Type: text/css\r\n" + "Content-Length: %d\r\nConnection: close\r\n\r\n", g_css_len); + write(fd, hdr, hl); + write(fd, g_css, g_css_len); +} + +void send_static_js(int fd, const char *path) { + for (int i = 0; g_js[i].url_path; i++) { + if (strcmp(g_js[i].url_path, path) == 0) { + if (!g_js[i].data) { send_404(fd); return; } + char hdr[256]; + int hl = snprintf(hdr, sizeof(hdr), + "HTTP/1.1 200 OK\r\nContent-Type: application/javascript\r\n" + "Content-Length: %d\r\nConnection: close\r\n\r\n", g_js[i].len); + write(fd, hdr, hl); + write(fd, g_js[i].data, g_js[i].len); + return; + } + } + send_404(fd); +} + +void send_json(int fd, const char *json) { + char hdr[256]; + snprintf(hdr, sizeof(hdr), + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Length: %zu\r\nConnection: close\r\n\r\n", strlen(json)); + write(fd, hdr, strlen(hdr)); + write(fd, json, strlen(json)); +} + +void send_json_buf(int fd, Buf *b) { + char hdr[256]; + snprintf(hdr, sizeof(hdr), + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Length: %zu\r\nConnection: close\r\n\r\n", b->len); + write(fd, hdr, strlen(hdr)); + write(fd, b->data, b->len); +} + +void send_404(int fd) { + const char *r = "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found"; + write(fd, r, strlen(r)); +} diff --git a/http.h b/http.h new file mode 100644 index 0000000..4181fc8 --- /dev/null +++ b/http.h @@ -0,0 +1,31 @@ +#pragma once +#include "buf.h" + +typedef struct { + char method[8]; + char path[256]; + char query[1024]; + char body[65536]; + int body_len; +} Req; + +/* Template globals — initialised by http_load_templates() */ +extern char *g_header; +extern int g_header_len; +extern char *g_footer; +extern int g_footer_len; +extern char *g_css; +extern int g_css_len; + +/* Static JS files served verbatim */ +typedef struct { const char *url_path; const char *fs_name; char *data; int len; } StaticJS; +extern StaticJS g_js[]; + +int http_load_templates(void); +int parse_request(int fd, Req *req); +void send_page(int fd, const char *title, Buf *body, const char *script_src); +void send_css(int fd); +void send_static_js(int fd, const char *path); +void send_json(int fd, const char *json); +void send_json_buf(int fd, Buf *b); +void send_404(int fd); diff --git a/http.o b/http.o new file mode 100644 index 0000000000000000000000000000000000000000..7d6ebbbaca05f15aa470bb7e06498cba572214bb GIT binary patch literal 10192 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 literal 0 HcmV?d00001 diff --git a/iptv.css b/iptv.css new file mode 100644 index 0000000..7ef47be --- /dev/null +++ b/iptv.css @@ -0,0 +1,41 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&display=swap'); +:root{--black:#000;--offblack:#0a0a0a;--terminal:#ccc;--dim:#555;--border:#171717;--strawberry:#e8547a;--green:#4ade80} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--black);color:var(--terminal);font-family:'JetBrains Mono',monospace;min-height:100vh;padding:24px 16px 48px;-webkit-font-smoothing:antialiased} +header{border-bottom:1px solid var(--border);padding-bottom:20px;margin-bottom:24px;display:flex;align-items:baseline;gap:16px} +header h1{font-size:22px;font-weight:700;letter-spacing:-.04em;color:#fff} +nav{display:flex;gap:6px;flex-wrap:wrap} +nav a{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);text-decoration:none;padding:4px 10px;border:1px solid var(--border);border-radius:6px;transition:border-color .15s,color .15s} +nav a:hover{border-color:var(--strawberry);color:var(--strawberry)} +.c{max-width:960px;margin:0 auto} +.lbl{font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin-bottom:12px} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-bottom:28px} +.card{background:var(--offblack);border:1px solid var(--border);border-radius:12px;padding:14px 12px 10px;cursor:pointer;text-decoration:none;display:block;color:var(--terminal);transition:border-color .15s} +.card:hover{border-color:#2a2a2a} +.card:active{background:#0f0f0f} +.card-img{width:100%;aspect-ratio:2/3;background:var(--border);border-radius:6px;overflow:hidden;margin-bottom:8px} +.card-img img{width:100%;height:100%;object-fit:cover;display:block} +.card-name{font-size:12px;font-weight:700;color:#fff;letter-spacing:-.01em;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.card-sub{font-size:10px;color:var(--dim);letter-spacing:.04em} +table{width:100%;border-collapse:collapse;font-size:12px} +th{text-align:left;padding:8px 10px;background:var(--offblack);border-bottom:1px solid var(--border);font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);position:sticky;top:0} +td{padding:8px 10px;border-bottom:1px solid var(--border);vertical-align:middle} +tr:hover td{background:var(--offblack)} +.btn{display:inline-block;padding:4px 12px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:10px;font-family:inherit;letter-spacing:.06em;text-transform:uppercase;text-decoration:none;transition:border-color .15s,color .15s;background:transparent;color:var(--terminal)} +.btn:hover{border-color:var(--strawberry);color:var(--strawberry)} +.btn-g{border-color:var(--green);color:var(--green)}.btn-g:hover{border-color:#22d360;color:#22d360} +.btn-s{border-color:var(--strawberry);color:var(--strawberry)}.btn-r{border-color:#ef4444;color:#ef4444}.btn-r:hover{border-color:#f87171;color:#f87171} +input[type=checkbox]{cursor:pointer;accent-color:var(--strawberry)} +.search{width:100%;padding:8px 12px;background:var(--offblack);border:1px solid var(--border);color:var(--terminal);border-radius:8px;font-size:12px;font-family:inherit;margin-bottom:14px;outline:none;transition:border-color .15s} +.search:focus{border-color:#2a2a2a} +select{background:var(--offblack);border:1px solid var(--border);color:var(--terminal);border-radius:8px;font-size:12px;font-family:inherit;padding:8px 12px;outline:none;cursor:pointer} +.pbar{background:var(--border);border-radius:2px;height:2px;overflow:hidden;min-width:80px;display:inline-block;vertical-align:middle} +.pfill{background:var(--green);height:100%} +.done{color:var(--green)}.err{color:var(--strawberry)}.dl{color:#fbbf24} +.bc{font-size:10px;color:var(--dim);letter-spacing:.04em;margin-bottom:16px} +.bc a{color:var(--strawberry);text-decoration:none} +.sep{border:none;border-top:1px solid var(--border);margin:20px 0} +.sform{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap} +.sform input{flex:1;min-width:160px;margin-bottom:0} +.sform select{margin-bottom:0} +@media(min-width:480px){.grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr))}} diff --git a/iptv_api.c b/iptv_api.c new file mode 100644 index 0000000..494bc34 --- /dev/null +++ b/iptv_api.c @@ -0,0 +1,26 @@ +#include "iptv_api.h" +#include "config.h" +#include "buf.h" +#include +#include + +static size_t curl_write_buf(char *ptr, size_t sz, size_t n, void *ud) { + buf_append((Buf*)ud, ptr, sz*n); return sz*n; +} + +char *api_get(const char *action, const char *extra) { + CURL *c = curl_easy_init(); + Buf b; buf_init(&b); + char url[1024]; + snprintf(url, sizeof(url), "%s?username=%s&password=%s&action=%s%s", + g_cfg.iptv_api, g_cfg.iptv_user, g_cfg.iptv_pass, + action, extra ? extra : ""); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, curl_write_buf); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &b); + curl_easy_setopt(c, CURLOPT_TIMEOUT, CURL_API_TIMEOUT); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_perform(c); + curl_easy_cleanup(c); + return b.data; /* caller frees */ +} diff --git a/iptv_api.h b/iptv_api.h new file mode 100644 index 0000000..2c2cb0e --- /dev/null +++ b/iptv_api.h @@ -0,0 +1,3 @@ +#pragma once +/* Fetch Xtream Codes API endpoint, return malloc'd JSON string (caller frees) */ +char *api_get(const char *action, const char *extra); diff --git a/iptv_api.o b/iptv_api.o new file mode 100644 index 0000000000000000000000000000000000000000..43921049607b6ca2956afa597022744b9bfd81d8 GIT binary patch literal 2688 zcmb<-^>JfjWMqH=Mg}_u1P><4z;J*K!FB*M9T@l-_!xpcIzM}K{`2TO@4>(RmPhBU z7a&QG&KuE(`Q=?07#u@9JO6|Rdo;c=U}0cL@#xl3)nH&?@aSBlqQL^9zd|)U_vkzf zVfM0ujM&EjlJ4aNv3+_w8<-dve0m!hm>3v5dPPBk9<6UZ_+4HY9td>|@#w8lF@Wk0 zQ8Do7yl;45pO88O16XXIwjao*`-TTRdPTke|Njpb_2>;z5rC)^gsI#m0(KinH^^eJ zx?P~)fT%NJfSTyhc^@nXr@LKL1fn86x~G7BmExf-0`_Jpk4NJXkjJBA9b+Kg138_6 zfkCy{zO*>CC@(QL)mF7wtst?uxIDio1;kEFF3HT#vsEotEoNYFcXqZ?&~Wz))l@Jv z(=*gFR4_6yGBYqSHh>t!!oa|wz`(!&3Nv@VPzELj#seTRkfT8=7}5A34}!(3f*2Sp z1Q?}x*f}OJGB8LmFfhn~Bp4VNq(QVJpFlHHBp(MTKo}SpEI?um3=D!GgPr&UdYD}J zB-)rA`4pO2-1#nWFop5m;Bex5z`@kUcZ0)`?*c~@7oUP7pM)cyfD<2wBR5n(E(&BU z3&Y3%*yNe9q(~kH29RIj%E4}ixg8uDa49eY6h;gTNQoFM%D|w?z`!5?Wg}Cd;1yzE z1cxn19PDpsIABxnj>DV)9O7X(#N%;@7vKPb?@%%}ZfOEXa&cPc4C{O-(GWjL*!=ECH(kGt%Re z)6yA=^9qVG^Gec?loh9z0T+E&7lpE?9qH--5^pf*)b5rw581(Y;OH%dR{X%uYX#vVl z&nwj{D9SHLEh?#G&;y&Fnh~E?l$e_ebswb^$X}qKK`$9#>A?^riDU_S-UpRCpfmwf z4^uh=B*?(PkO9>XN*f?1D9wV@fXZ7Ct;WCrF5Q?x93)%_QjElhG0}x}7#J8pW|FJl z5Nf{>%mOG4vmf0)7#~K1{0(BG>#qO_GB7ZxL(PLxAh&|pP-zB#sQsYw79%HP%}Y!1SSTe zKS1?^jwZlx_dPM literal 0 HcmV?d00001 diff --git a/jellyfin.c b/jellyfin.c new file mode 100644 index 0000000..d777d58 --- /dev/null +++ b/jellyfin.c @@ -0,0 +1,74 @@ +#include "jellyfin.h" +#include "config.h" +#include +#include +#include +#include +#include +#include +#include + +void trigger_jellyfin_scan(void) { + if (!g_cfg.jellyfin_url[0]) return; + CURL *c = curl_easy_init(); + if (!c) return; + curl_easy_setopt(c, CURLOPT_URL, g_cfg.jellyfin_url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, 0L); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); + curl_easy_perform(c); + curl_easy_cleanup(c); +} + +static int cmp_str(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } + +void update_show_manifest(const char *season_dir, const char *title_hint) { + (void)title_hint; + char show_dir[512]; + strncpy(show_dir, season_dir, sizeof(show_dir)-1); show_dir[sizeof(show_dir)-1] = 0; + char *slash = strrchr(show_dir, '/'); + if (!slash) return; + *slash = 0; + const char *title = strrchr(show_dir, '/'); + title = title ? title+1 : show_dir; + + char manifest[640]; snprintf(manifest, sizeof(manifest), "%s/show.json", show_dir); + + char **files = NULL; int fc = 0, fcap = 0; + DIR *sd = opendir(show_dir); + if (!sd) return; + struct dirent *se; + while ((se = readdir(sd))) { + if (strncmp(se->d_name, "Season ", 7) != 0) continue; + char sdir[640]; snprintf(sdir, sizeof(sdir), "%s/%s", show_dir, se->d_name); + DIR *ed = opendir(sdir); + if (!ed) continue; + struct dirent *ee; + while ((ee = readdir(ed))) { + if (ee->d_name[0] == '.') continue; + char fpath[768]; snprintf(fpath, sizeof(fpath), "%s/%s", sdir, ee->d_name); + struct stat st; if (stat(fpath, &st) != 0 || !S_ISREG(st.st_mode)) continue; + char entry[900]; + snprintf(entry, sizeof(entry), "{\"file\":\"%s/%s\",\"size\":%lld}", + se->d_name, ee->d_name, (long long)st.st_size); + if (fc >= fcap) { fcap = fcap ? fcap*2 : 32; files = realloc(files, fcap*sizeof(char*)); } + files[fc++] = strdup(entry); + } + closedir(ed); + } + closedir(sd); + + if (fc > 1) qsort(files, fc, sizeof(char*), cmp_str); + + FILE *f = fopen(manifest, "w"); + if (f) { + fprintf(f, "{\"title\":\"%s\",\"updated\":%ld,\"count\":%d,\"episodes\":[", + title, (long)time(NULL), fc); + for (int i = 0; i < fc; i++) { if (i) fputc(',', f); fputs(files[i], f); } + fputs("]}", f); + fclose(f); + } + for (int i = 0; i < fc; i++) free(files[i]); + free(files); +} diff --git a/jellyfin.h b/jellyfin.h new file mode 100644 index 0000000..9437c99 --- /dev/null +++ b/jellyfin.h @@ -0,0 +1,3 @@ +#pragma once +void trigger_jellyfin_scan(void); +void update_show_manifest(const char *season_dir, const char *title_hint); diff --git a/jellyfin.o b/jellyfin.o new file mode 100644 index 0000000000000000000000000000000000000000..025925f49b0053723f75a404b0daff8d71c3f87b GIT binary patch literal 5600 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 Rl3-w9fVGogdeDqx008xTiE01< literal 0 HcmV?d00001 diff --git a/json.c b/json.c new file mode 100644 index 0000000..1d02602 --- /dev/null +++ b/json.c @@ -0,0 +1,116 @@ +#include "json.h" +#include +#include +#include +#include + +char *json_str(const char *json, const char *key) { + if (!json || !key) return NULL; + char needle[128]; snprintf(needle, sizeof(needle), "\"%s\"", key); + const char *p = strstr(json, needle); + if (!p) return NULL; + p += strlen(needle); + while (*p == ' ' || *p == ':') p++; + if (*p == '"') { + p++; + const char *end = p; + while (*end && !(*end == '"' && *(end-1) != '\\')) end++; + size_t len = end - p; + char *r = malloc(len+1); memcpy(r, p, len); r[len] = 0; return r; + } + const char *end = p; + while (*end && *end != ',' && *end != '}' && *end != ']') end++; + size_t len = end - p; + char *r = malloc(len+1); memcpy(r, p, len); r[len] = 0; return r; +} + +char **json_array(const char *json, int *n) { + *n = 0; + if (!json) return NULL; + const char *p = json; + while (*p && *p != '[') p++; + if (!*p) return NULL; + p++; + int cap = 64; + char **arr = malloc(cap * sizeof(char*)); + int depth = 0; const char *obj_start = NULL; int in_str = 0; + for (; *p; p++) { + if (*p == '"' && (p == json || *(p-1) != '\\')) { in_str = !in_str; continue; } + if (in_str) continue; + if (*p == '{' || *p == '[') { if (depth == 0) obj_start = p; depth++; } + else if (*p == '}' || *p == ']') { + depth--; + if (depth == 0 && obj_start) { + size_t len = p - obj_start + 1; + char *obj = malloc(len+1); memcpy(obj, obj_start, len); obj[len] = 0; + if (*n >= cap) { cap *= 2; arr = realloc(arr, cap*sizeof(char*)); } + arr[(*n)++] = obj; + obj_start = NULL; + if (*p == ']') break; + } + } + } + return arr; +} + +void urldecode(char *s) { + char *w = s, *r = s; + while (*r) { + if (*r == '%' && r[1] && r[2]) { + char hex[3] = {r[1], r[2], 0}; *w++ = (char)strtol(hex, NULL, 16); r += 3; + } else if (*r == '+') { *w++ = ' '; r++; } + else { *w++ = *r++; } + } + *w = 0; +} + +char *qparam(const char *qs, const char *key) { + if (!qs || !key) return NULL; + char needle[128]; snprintf(needle, sizeof(needle), "%s=", key); + const char *p = strstr(qs, needle); + if (!p) return NULL; + p += strlen(needle); + const char *end = strchr(p, '&'); + size_t len = end ? (size_t)(end-p) : strlen(p); + char *r = malloc(len+1); memcpy(r, p, len); r[len] = 0; + urldecode(r); + return r; +} + +void html_esc(Buf *b, const char *s) { + for (; *s; s++) { + if (*s == '<') buf_str(b, "<"); + else if (*s == '>') buf_str(b, ">"); + else if (*s == '&') buf_str(b, "&"); + else if (*s == '"') buf_str(b, """); + else buf_append(b, s, 1); + } +} + +void js_esc(Buf *b, const char *s) { + for (; *s; s++) { + if (*s == '\'') buf_str(b, "\\'"); + else if (*s == '\\') buf_str(b, "\\\\"); + else if (*s == '\n' || *s == '\r') {} + else buf_append(b, s, 1); + } +} + +void fwrite_json_str(FILE *f, const char *s) { + fputc('"', f); + for (; *s; s++) { + if (*s == '"' || *s == '\\') fputc('\\', f); + if ((unsigned char)*s >= 0x20) fputc(*s, f); + } + fputc('"', f); +} + +int str_icontains(const char *hay, const char *needle) { + if (!hay || !needle || !*needle) return 1; + char h[2048], n[256]; + strncpy(h, hay, sizeof(h)-1); h[sizeof(h)-1] = 0; + strncpy(n, needle, sizeof(n)-1); n[sizeof(n)-1] = 0; + for (char *p = h; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; + for (char *p = n; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; + return strstr(h, n) != NULL; +} diff --git a/json.h b/json.h new file mode 100644 index 0000000..4da0a44 --- /dev/null +++ b/json.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include "buf.h" + +/* JSON micro-parser */ +char *json_str(const char *json, const char *key); +char **json_array(const char *json, int *n); + +/* URL helpers */ +void urldecode(char *s); +char *qparam(const char *qs, const char *key); + +/* Escaping */ +void html_esc(Buf *b, const char *s); +void js_esc(Buf *b, const char *s); +void fwrite_json_str(FILE *f, const char *s); + +/* String utils */ +int str_icontains(const char *hay, const char *needle); diff --git a/json.o b/json.o new file mode 100644 index 0000000000000000000000000000000000000000..ca8e137b9e7fd1673676ec059bb74384df51ca8c GIT binary patch literal 6368 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 literal 0 HcmV?d00001 diff --git a/main.c b/main.c new file mode 100644 index 0000000..ba7faf0 --- /dev/null +++ b/main.c @@ -0,0 +1,80 @@ +/* + * iptv-dl — IPTV Downloader + * Config: ~/.iptv-downloader/config.json (or $IPTV_DL_CONFIG, or --config PATH) + * Build: make + */ +#include "config.h" +#include "http.h" +#include "queue.h" +#include "notify.h" +#include "server.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + const char *cfg_path = NULL; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--config") && i+1 < argc) cfg_path = argv[++i]; + else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { + fprintf(stderr, + "Usage: %s [--config PATH] [--dump-config]\n" + " --config PATH load config from PATH\n" + " --dump-config print active config and exit\n" + "Config search order:\n" + " --config arg → $IPTV_DL_CONFIG → ~/.iptv-downloader/config.json\n" + " → /etc/iptv-downloader/config.json → built-in defaults\n", + argv[0]); + return 0; + } else if (!strcmp(argv[i], "--dump-config")) { + config_load(cfg_path); + config_dump(); + return 0; + } + } + + config_load(cfg_path); + + if (!http_load_templates()) { + fprintf(stderr, "iptv-dl: failed to load templates from %s/\n", g_cfg.template_dir); + return 1; + } + + mkdir(g_cfg.data_dir, 0755); + history_load(); + notify_load(); + clean_partials(); + + curl_global_init(CURL_GLOBAL_ALL); + signal(SIGPIPE, SIG_IGN); + + pthread_t worker; + pthread_create(&worker, NULL, dl_worker, NULL); + pthread_detach(worker); + + int srv = socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)g_cfg.port); + addr.sin_addr.s_addr = INADDR_ANY; + if (bind(srv, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; } + listen(srv, BACKLOG); + fprintf(stderr, "iptv-dl :%d (template_dir=%s data_dir=%s)\n", + g_cfg.port, g_cfg.template_dir, g_cfg.data_dir); + + for (;;) { + struct sockaddr_in cli; socklen_t clen = sizeof(cli); + int cfd = accept(srv, (struct sockaddr*)&cli, &clen); + if (cfd < 0) continue; + int *p = malloc(sizeof(int)); *p = cfd; + pthread_t tid; pthread_create(&tid, NULL, handle_conn, p); pthread_detach(tid); + } +} diff --git a/main.o b/main.o new file mode 100644 index 0000000000000000000000000000000000000000..1439e11ef14bc0fe22264908543d5bc345efd1fe GIT binary patch literal 5272 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 F000|!&i()Z literal 0 HcmV?d00001 diff --git a/notify.c b/notify.c new file mode 100644 index 0000000..bc10145 --- /dev/null +++ b/notify.c @@ -0,0 +1,97 @@ +#include "notify.h" +#include "config.h" +#include "json.h" +#include +#include +#include +#include + +Notification g_notifs[MAX_NOTIF]; +int g_notif_count = 0; +int g_notif_tail = 0; + +static pthread_mutex_t notif_mutex = PTHREAD_MUTEX_INITIALIZER; + +void notify_add(const char *msg, const char *type) { + pthread_mutex_lock(¬if_mutex); + Notification *nf = &g_notifs[g_notif_tail]; + nf->ts = time(NULL); + strncpy(nf->type, type, sizeof(nf->type)-1); + strncpy(nf->msg, msg, sizeof(nf->msg)-1); + g_notif_tail = (g_notif_tail + 1) % MAX_NOTIF; + if (g_notif_count < MAX_NOTIF) g_notif_count++; + pthread_mutex_unlock(¬if_mutex); + notify_save(); +} + +void notify_save(void) { + char path[512]; config_notif_path(path, sizeof(path)); + FILE *f = fopen(path, "w"); + if (!f) return; + pthread_mutex_lock(¬if_mutex); + fputc('[', f); + int start = (g_notif_count < MAX_NOTIF) ? 0 : g_notif_tail; + for (int i = 0; i < g_notif_count; i++) { + Notification *n = &g_notifs[(start + i) % MAX_NOTIF]; + if (i > 0) fputc(',', f); + fprintf(f, "{\"ts\":%ld,\"type\":\"%s\",\"msg\":", (long)n->ts, n->type); + fwrite_json_str(f, n->msg); + fputc('}', f); + } + fprintf(f, "]\n"); + pthread_mutex_unlock(¬if_mutex); + fclose(f); +} + +void notify_load(void) { + char path[512]; config_notif_path(path, sizeof(path)); + FILE *f = fopen(path, "r"); + if (!f) return; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + char *buf = malloc(sz + 1); + if (!buf) { fclose(f); return; } + fread(buf, 1, sz, f); buf[sz] = 0; fclose(f); + int n; char **arr = json_array(buf, &n); + free(buf); + if (!arr) return; + for (int i = 0; i < n; i++) { + char *ts_s = json_str(arr[i], "ts"); + char *type = json_str(arr[i], "type"); + char *msg = json_str(arr[i], "msg"); + if (ts_s && type && msg) { + Notification *nf = &g_notifs[g_notif_tail]; + nf->ts = (time_t)atol(ts_s); + strncpy(nf->type, type, sizeof(nf->type)-1); + strncpy(nf->msg, msg, sizeof(nf->msg)-1); + g_notif_tail = (g_notif_tail + 1) % MAX_NOTIF; + if (g_notif_count < MAX_NOTIF) g_notif_count++; + } + free(ts_s); free(type); free(msg); free(arr[i]); + } + free(arr); + fprintf(stderr, "iptv-dl: loaded %d notifications\n", g_notif_count); +} + +int notify_dismiss(time_t ts) { + pthread_mutex_lock(¬if_mutex); + int start = (g_notif_count < MAX_NOTIF) ? 0 : g_notif_tail; + int found = 0; + for (int i = 0; i < g_notif_count; i++) { + int idx = (start + i) % MAX_NOTIF; + if (g_notifs[idx].ts == ts) { + for (int j = i; j < g_notif_count - 1; j++) { + int cur = (start + j) % MAX_NOTIF; + int nxt = (start + j + 1) % MAX_NOTIF; + g_notifs[cur] = g_notifs[nxt]; + } + g_notif_count--; + if (g_notif_tail == 0) g_notif_tail = MAX_NOTIF - 1; + else g_notif_tail--; + found = 1; + break; + } + } + pthread_mutex_unlock(¬if_mutex); + if (found) notify_save(); + return found; +} diff --git a/notify.h b/notify.h new file mode 100644 index 0000000..3d8d693 --- /dev/null +++ b/notify.h @@ -0,0 +1,19 @@ +#pragma once +#include + +typedef struct { + time_t ts; + char type[16]; /* "done" | "error" */ + char msg[512]; +} Notification; + +extern Notification g_notifs[/* MAX_NOTIF */]; +extern int g_notif_count; +extern int g_notif_tail; + +void notify_add(const char *msg, const char *type); +void notify_save(void); +void notify_load(void); + +/* Dismiss by timestamp; returns 1 if found */ +int notify_dismiss(time_t ts); diff --git a/notify.o b/notify.o new file mode 100644 index 0000000000000000000000000000000000000000..b48e512830f4b9f6340327f685256fe2d643ad73 GIT binary patch literal 7232 zcmb<-^>JfjWMqH=Mg}_u1P><4z|bIpU^{@B4h;MZd<-6qZ!DM?7*agCO;j})7#KV{ zx2SM1F)-|7U;=TTzW}NA=rsjXoltJ;0sfXyMv(j(6$y~KUR#L#QJ>B&Dh5mp3=nOd z@AgMCKsnv4ASKN|4Eg&l7#SE^5AgS01=}}AMSzKc!PW4aPv;U91ttatpUyQZ8cYlf zp2_!IIzRaI>Vh=78b0yJye;4QI`H5Rd5?p?RWuK0A2xF7{H%HSwGO{LgL5y7iAU$V z{W>5slW)v~Dh8{(oPF6y^Frrgh@+Bk!@cv~@PJ3=ZIBNfn~yMhc8AC^c=r0pa)1?j z_Lj&>cyzu8b3jJd!o1>QsS0vZeYZz9#PL4eP?vW@T<+2Nau*W=1H(R0On5*72Vx8| z8)kGjBoKYNAwlQS-2#q*6c23{aFmqldo&&aMbcq@c?OS8h?Oqg;3z>4vzN#J{r?YB zb&TY zc>o-%Kjl3R{!r08k$u2O^AJQyrzpst<{yUjoXrOqJHZJE>NK!H9?eGtqGKInUYh^? z|DRvJ0i@ca@l6DJdIm)>Bt7@ms0cu`LQ^zC0F3wq5oh=f6o#Ng=+SxHqw_E* zBtgj<6x^VgfSA#FA50rw0x7@m*?9!a_3W)tVFa^0I*)pQjCBWPk(wxr1?8Js$IaWOD3fHF4JExo*; zD1s!G&Ue0<-#{_r)mvl8;nm4v2sY6Jss@y5d^+EOBqhKSkR$<3HP>LN<{u>0e884! z`1`;A`~M%Fa$dgt_y7L{pYAm(;Na?p1eZ@YBqTkck=YGSa~|E`0?qIMa*%*>J2**n zuK|~Fpj;1144@1P@(3css(3UW0j0vjX%ld1dVLa+!l1@=E>Qs)haRCmoe&p-LjmSY zT*ki4|NsC01f=}z(fmfkr!z-I!n5-)H1GIymZ&IzO@kI9j?MoJ`TMLu`Lt6;1yZnh zbmpjV_;jYI2pFF9=>?Z}ujOHd1d{p92N@kL&++%kFfuUo${2bizwW%{k$DuHrGG&p z=R)T#aP)X2AM(gN017EzOC6TdqmIq*7+ows*5X^ZdhU)FSbr7aI`+`yDjpoCQ zAfxYTUg*5#k^BRy3Zy3UKgdLI*>S;#@%b(g4`kgjkK}J2kQC;_2q}0RE$^1jhomEy zPH+NkJ;2{O2~>1J%H`wWBE}F_&V$3qqgzJBqdP~1V+W`(_vubi5%B5GQIYWJE>ThN zXg;EG7@Xf+Tffz5dUVRDfXV_`(7inH@BjZausqxtunEwZ09$}C&F}!iyyh2-P(_e( z53KZM*8l(iL0O1_p`4*wsiauRN;M}%N2#Q;AXUjqNwru>M=7^BUCD|emW!c?p`@4r zB*TzfoX(J0P*SFwl4GTilb@K9nxdeZqL7zgl9`s7oLG{XpI6Mqz~JueY^9*#?iZ@5 zU}&aisAs5PWME`wU}9{*z`(%5z`&ruz`y_sA$Pw}1||l^10XR_pnzP&h{hKI2{JG+ zFjfUIFjfdKO7pODOkiYS2w-4fkb%j8>PUCK11wCgZLHpXOiV7#Y&|T!%uK5edz|q& zTYAjnTofON3*P~jHr76-c!;Fy;bUjcp37)vYhwke?PFqK0M+>gP&0ReXeT~_9wu)- zi8f{rK80o$7d``~>3j-~d=id)0#1A!F$@e03qa~X{$gfeU~uPqz{2Fy%pTmv*2Buw z(a#jz%hJcptj>7WBO)pf(_gB?cnEu?=Rx3288k71TNe zvtR@$ZGcjTJWK#egG&wuRt6;~A4X|`8LSK{U>ZT_gPE)h>R=i{n1GqA;J5}ep@b!v zW&@MRgdGC|gAh_UFoMz=0|SE>0|SEqvO*{ulqRr=C*e?EibK4bfq_AgfdwAYpgaPy zq7y0(2`2`I4bbogl|I<)orS~vRXD^q;t)TEL;Nh%Js`J%FwC8|aHxL`4Np*89i$$d z&lngOzTr^+8;3YEBP3ofK;wf2l>ZnQ82A~n$BQTqacLalN;t$daEKe@5MPMH{h+=J zw)pbEVU9o4zYjnKCj$cmxIAEBV2Hz^o&j7cSL!8081cEKC8-q*dOpquAi@wt7=Z|5 z5Mcr$OhJShh%krfsf;g9EK6lb&d*EBOpgbf7hjNAlEILcUyz!|P*9Rll$w|VF*!ab zKRKHrt)R3dnE|FYIlnZo1jZ|7NGmAH%qvM_NGmVOEJ=;eD$dV~FD@y9^3jYd&4U=2 zoReRi3Ns_VBr!9GAwIsiBr!QVJ~<;hJ`E%Qc3EY7VoC}_NoH;;Lvcw_UUESt%oCuB ziXp8yH8q{i;5B}K?11^@$s3(`N>AcAg7h&=P(qP zq@)%V!OTj@EY8g=E@sG0%>~)Oz`&pgO2nY@pMimakAZ>V&wmJjm4Be*35qh9IH(Q; zX#wREP-h1w4le5%7{KKrC@J58=IaQMVyO9`^a4|#iX<+GWN$W99AvK$lK2uNabYC! zw@`7Adyvif4iyKP11c3^_KJZj45)iRbu&y{8A%+}^o5BVAc-TJZw?g)nUCzwR3veB zBzI;(#X;@_)hRIZ3!&m5bCAQW3@Q#%FOFnR9g;X`C3!|kN`BjA&37wBynW*Ymmf|)o(@;M-GP_P;pSWA&38GByr^Y@(U^sqBxMk z8CI@?%t6jC<{-ZM#F5oUAc=$WGE99rk~pZ%0TXXU5=Txy zlaR!fk=#EQNgUKhfthm$hxij5;%{(>|HL8A4+=17{Bk0>M+7PkqCjmbn0w?w0#Nm! zwj4}c9V!l@kkuQ31fc4rq8QEEu$XGwN4^$jP zA*cVzAOS4umqNus6teo&AOWa)WcQpy5=TzwSCGV!)jvZLM^?`RD)gc06FJ_6pyHr( zj;vl1NgUK>g@wNok~j}i_^3n0LDnOe!>f_Rk;|*iNaD!p_B~V_$n zq2lP~fXXgdJpwWd#0KFK5CQcsNF3IVMy}sM;_!AcwEhB#fiSH7$e>qTnOl;W#GqGP zQUswhV639loJ75n)QS=Yy_Cd~LHZlyu_>3OAk1x5JSmQ#0bziV|~S?HH)H$fiKy1@bC1nZV6~ z%uz5fY=VX}OadkjfdP~ULH0no44}LOG6Ph0fy6;+ z1jL8oPiXeS#6fCdY!D3^djOe(>A|ih%*6-whRp_l*5PV>Ap54CwZFK^219Fd+M27-ScS4WmKr zTo@Z94#R0s{V+a^Mpp}JE`x#!l*VCdKy(IFKf1f1!XPDJ3>x$YnTf7{22>%aUj|kP zC7{Ak9s>iYzm6^aOaK)!3=9kjP?ezc1FBzP`eEq@o82HS4?vYS0|UbcXu<%MO(2Y} sAEXSb1f&g)FMt#?FfhQ{)i4Q=eK0nN2DQzx>Hh$Ae-}s*5=Pe#0EY)on*aa+ literal 0 HcmV?d00001 diff --git a/queue.c b/queue.c new file mode 100644 index 0000000..3fd045a --- /dev/null +++ b/queue.c @@ -0,0 +1,301 @@ +#include "queue.h" +#include "config.h" +#include "json.h" +#include "discord.h" +#include "notify.h" +#include "jellyfin.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Download g_downloads[MAX_DL]; +int g_dl_count = 0; +pthread_mutex_t g_dl_mutex = PTHREAD_MUTEX_INITIALIZER; + +QNode *g_q_head = NULL; +QNode *g_q_tail = NULL; +pthread_mutex_t g_q_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_cond_t g_q_cond = PTHREAD_COND_INITIALIZER; + +/* ── History ──────────────────────────────────────────────────── */ + +void history_save(void) { + char path[512]; config_history_path(path, sizeof(path)); + FILE *f = fopen(path, "w"); + if (!f) return; + pthread_mutex_lock(&g_dl_mutex); + fputc('[', f); + for (int i = 0; i < g_dl_count; i++) { + if (i > 0) fputc(',', f); + fprintf(f, "{\"id\":"); fwrite_json_str(f, g_downloads[i].id); + fprintf(f, ",\"name\":"); fwrite_json_str(f, g_downloads[i].name); + fprintf(f, ",\"status\":"); fwrite_json_str(f, g_downloads[i].status); + fprintf(f, ",\"cover_url\":"); fwrite_json_str(f, g_downloads[i].cover_url); + fprintf(f, ",\"url\":"); fwrite_json_str(f, g_downloads[i].url); + fprintf(f, ",\"dest_dir\":"); fwrite_json_str(f, g_downloads[i].dest_dir); + fprintf(f, ",\"filename\":"); fwrite_json_str(f, g_downloads[i].filename); + fprintf(f, ",\"downloaded\":%ld,\"total\":%ld}", g_downloads[i].downloaded, g_downloads[i].total); + } + fprintf(f, "]\n"); + pthread_mutex_unlock(&g_dl_mutex); + fclose(f); +} + +void history_load(void) { + char path[512]; config_history_path(path, sizeof(path)); + FILE *f = fopen(path, "r"); + if (!f) return; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + char *buf = malloc(sz + 1); + if (!buf) { fclose(f); return; } + fread(buf, 1, sz, f); buf[sz] = 0; fclose(f); + + int n; char **arr = json_array(buf, &n); + free(buf); + if (!arr) return; + + for (int i = 0; i < n && g_dl_count < MAX_DL; i++) { + char *id = json_str(arr[i], "id"); + char *name = json_str(arr[i], "name"); + char *status = json_str(arr[i], "status"); + char *cover = json_str(arr[i], "cover_url"); + char *url = json_str(arr[i], "url"); + char *destdir = json_str(arr[i], "dest_dir"); + char *fname = json_str(arr[i], "filename"); + char *dl_s = json_str(arr[i], "downloaded"); + char *tot_s = json_str(arr[i], "total"); + + if (id && name && status) { + Download *d = &g_downloads[g_dl_count++]; + d->active = 1; d->cancelled = 0; + strncpy(d->id, id, 63); + strncpy(d->name, name, 255); + strncpy(d->status, status, 63); + strncpy(d->cover_url,cover?cover:"", 511); + strncpy(d->url, url?url:"", 511); + strncpy(d->dest_dir, destdir?destdir:"", 511); + strncpy(d->filename, fname?fname:"", 255); + d->downloaded = dl_s ? atol(dl_s) : 0; + d->total = tot_s ? atol(tot_s) : 0; + d->speed_bps = 0; d->speed_ts = 0; d->speed_bytes = 0; + if (strcmp(d->status,"downloading")==0 || strcmp(d->status,"queued")==0) + strncpy(d->status, "interrupted", 63); + } + free(id); free(name); free(status); free(cover); + free(url); free(destdir); free(fname); free(dl_s); free(tot_s); + free(arr[i]); + } + free(arr); + fprintf(stderr, "iptv-dl: loaded %d history entries\n", g_dl_count); +} + +void clean_partials(void) { + char cmd[512]; + snprintf(cmd, sizeof(cmd), "find '%s' '%s' -name '*.part' -delete 2>/dev/null", + g_cfg.dl_dir_tv, g_cfg.dl_dir_mov); + system(cmd); +} + +/* ── Download worker ──────────────────────────────────────────── */ + +static int xprogress(void *ud, curl_off_t total, curl_off_t now, + curl_off_t ul_total, curl_off_t ul_now) { + (void)ul_total; (void)ul_now; + const char *id = (const char*)ud; + int cancel = 0; + time_t cur = time(NULL); + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, id) == 0) { + g_downloads[i].downloaded = (long)now; + g_downloads[i].total = (long)total; + cancel = g_downloads[i].cancelled; + if (g_downloads[i].speed_ts && cur != g_downloads[i].speed_ts) { + long dt = (long)(cur - g_downloads[i].speed_ts); + long db = (long)now - g_downloads[i].speed_bytes; + if (dt > 0 && db >= 0) g_downloads[i].speed_bps = (float)db / dt; + } + if (cur != g_downloads[i].speed_ts) { + g_downloads[i].speed_ts = cur; + g_downloads[i].speed_bytes = (long)now; + } + break; + } + } + pthread_mutex_unlock(&g_dl_mutex); + return cancel; +} + +static void do_download(DlTask *t) { + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", t->dest_dir); + system(cmd); + + char dest[768]; + snprintf(dest, sizeof(dest), "%s/%s", t->dest_dir, t->filename); + char dest_part[780]; + snprintf(dest_part, sizeof(dest_part), "%s.part", dest); + + pthread_mutex_lock(&g_dl_mutex); + int already_cancelled = 0; + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, t->id) == 0) { + if (g_downloads[i].cancelled) { + strncpy(g_downloads[i].status, "cancelled", 63); + already_cancelled = 1; + } else { + strncpy(g_downloads[i].status, "downloading", 63); + g_downloads[i].speed_bps = 0; g_downloads[i].speed_ts = 0; g_downloads[i].speed_bytes = 0; + } + break; + } + } + pthread_mutex_unlock(&g_dl_mutex); + if (already_cancelled) { history_save(); free(t); return; } + history_save(); + + CURL *c = curl_easy_init(); + FILE *f = fopen(dest_part, "wb"); + if (!f) { + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, t->id) == 0) { + snprintf(g_downloads[i].status, 63, "error: can't open file"); break; + } + } + pthread_mutex_unlock(&g_dl_mutex); + history_save(); curl_easy_cleanup(c); free(t); return; + } + + curl_easy_setopt(c, CURLOPT_URL, t->url); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT, CURL_DL_TIMEOUT); + curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, xprogress); + curl_easy_setopt(c, CURLOPT_XFERINFODATA, t->id); + curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L); + if (g_cfg.bind_iface[0]) + curl_easy_setopt(c, CURLOPT_INTERFACE, g_cfg.bind_iface); + if (g_cfg.max_recv_speed > 0) + curl_easy_setopt(c, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)g_cfg.max_recv_speed); + + CURLcode res = curl_easy_perform(c); + fclose(f); + curl_easy_cleanup(c); + struct stat _st; int _nonempty = (stat(dest_part, &_st) == 0 && _st.st_size > 0); + + char notify_msg[512] = ""; int scan_jf = 0; + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, t->id) == 0) { + g_downloads[i].speed_bps = 0; + if (g_downloads[i].cancelled) { + strncpy(g_downloads[i].status, "cancelled", 63); + remove(dest_part); + } else if (res == CURLE_OK && _nonempty) { + rename(dest_part, dest); + strncpy(g_downloads[i].status, "done", 63); + snprintf(notify_msg, sizeof(notify_msg), + ":white_check_mark: Downloaded: **%s**", t->name); + scan_jf = 1; + } else if (res == CURLE_OK && !_nonempty) { + strncpy(g_downloads[i].status, "error: empty file (bad url?)", 63); + remove(dest_part); + snprintf(notify_msg, sizeof(notify_msg), + ":x: Download failed: **%s** — empty file", t->name); + } else { + remove(dest_part); + snprintf(g_downloads[i].status, 63, "error: %s", curl_easy_strerror(res)); + snprintf(notify_msg, sizeof(notify_msg), + ":x: Download failed: **%s** — %s", t->name, curl_easy_strerror(res)); + } + break; + } + } + pthread_mutex_unlock(&g_dl_mutex); + history_save(); + if (notify_msg[0]) { + discord_notify(notify_msg); + notify_add(notify_msg, scan_jf ? "done" : "error"); + } + if (scan_jf) { trigger_jellyfin_scan(); update_show_manifest(t->dest_dir, t->name); } + free(t); +} + +void *dl_worker(void *arg) { + (void)arg; + for (;;) { + pthread_mutex_lock(&g_q_mutex); + while (!g_q_head) pthread_cond_wait(&g_q_cond, &g_q_mutex); + QNode *node = g_q_head; + g_q_head = g_q_head->next; + if (!g_q_head) g_q_tail = NULL; + pthread_mutex_unlock(&g_q_mutex); + do_download(node->task); + free(node); + } + return NULL; +} + +/* ── Queue entry point ────────────────────────────────────────── */ + +void queue_download(const char *stream_id, const char *filename, + const char *dest_dir, const char *name, + int is_movie, const char *cover_url) { + DlTask *t = calloc(1, sizeof(DlTask)); + if (is_movie) + snprintf(t->url, sizeof(t->url), "%s/movie/%s/%s/%s", + g_cfg.stream_base, g_cfg.iptv_user, g_cfg.iptv_pass, stream_id); + else + snprintf(t->url, sizeof(t->url), "%s/series/%s/%s/%s", + g_cfg.stream_base, g_cfg.iptv_user, g_cfg.iptv_pass, stream_id); + strncpy(t->dest_dir, dest_dir, sizeof(t->dest_dir)-1); + strncpy(t->filename, filename, sizeof(t->filename)-1); + strncpy(t->name, name, sizeof(t->name)-1); + strncpy(t->id, stream_id, sizeof(t->id)-1); + strncpy(t->cover_url, cover_url?cover_url:"", sizeof(t->cover_url)-1); + + char full_path[800]; + snprintf(full_path, sizeof(full_path), "%s/%s", dest_dir, filename); + struct stat _exist; + if (stat(full_path, &_exist) == 0 && _exist.st_size > 0) { free(t); return; } + + pthread_mutex_lock(&g_dl_mutex); + for (int i = 0; i < g_dl_count; i++) { + if (strcmp(g_downloads[i].id, stream_id) == 0 && + (strcmp(g_downloads[i].status, "queued") == 0 || + strcmp(g_downloads[i].status, "downloading") == 0)) { + pthread_mutex_unlock(&g_dl_mutex); + free(t); return; + } + } + if (g_dl_count < MAX_DL) { + Download *d = &g_downloads[g_dl_count++]; + d->active = 1; d->cancelled = 0; + strncpy(d->id, stream_id, 63); + strncpy(d->name, name, 255); + strncpy(d->status, "queued", 63); + strncpy(d->cover_url,cover_url?cover_url:"", 511); + strncpy(d->url, t->url, 511); + strncpy(d->dest_dir, dest_dir, 511); + strncpy(d->filename, filename, 255); + d->is_movie = is_movie; + d->downloaded = 0; d->total = 0; d->speed_bps = 0; d->speed_ts = 0; d->speed_bytes = 0; + } + pthread_mutex_unlock(&g_dl_mutex); + history_save(); + + QNode *node = malloc(sizeof(QNode)); + node->task = t; node->next = NULL; + pthread_mutex_lock(&g_q_mutex); + if (g_q_tail) g_q_tail->next = node; else g_q_head = node; + g_q_tail = node; + pthread_cond_signal(&g_q_cond); + pthread_mutex_unlock(&g_q_mutex); +} diff --git a/queue.h b/queue.h new file mode 100644 index 0000000..5f7841b --- /dev/null +++ b/queue.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include + +typedef struct { + int active; + int cancelled; + char id[64]; + char name[256]; + char status[64]; + char cover_url[512]; + char url[512]; + char dest_dir[512]; + char filename[256]; + int is_movie; + long downloaded; + long total; + time_t speed_ts; + long speed_bytes; + float speed_bps; +} Download; + +typedef struct { + char url[512]; + char dest_dir[512]; + char filename[256]; + char name[256]; + char id[64]; + char cover_url[512]; +} DlTask; + +typedef struct QNode { DlTask *task; struct QNode *next; } QNode; + +extern Download g_downloads[/* MAX_DL */]; +extern int g_dl_count; +extern pthread_mutex_t g_dl_mutex; + +extern QNode *g_q_head; +extern QNode *g_q_tail; +extern pthread_mutex_t g_q_mutex; +extern pthread_cond_t g_q_cond; + +/* Enqueue a download; skips if file exists or already queued/downloading */ +void queue_download(const char *stream_id, const char *filename, + const char *dest_dir, const char *name, + int is_movie, const char *cover_url); + +/* Worker thread entry point */ +void *dl_worker(void *arg); + +/* History persistence */ +void history_save(void); +void history_load(void); + +/* Delete orphaned .part files from both dl dirs */ +void clean_partials(void); diff --git a/queue.o b/queue.o new file mode 100644 index 0000000000000000000000000000000000000000..5c750d9a3b2199fe097c872f4d352c5df1835a5f GIT binary patch literal 17984 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 literal 0 HcmV?d00001 diff --git a/runit.run b/runit.run new file mode 100755 index 0000000..37cda0b --- /dev/null +++ b/runit.run @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/local/bin/iptv-dl 2>&1 diff --git a/server.c b/server.c new file mode 100644 index 0000000..fd6bcc2 --- /dev/null +++ b/server.c @@ -0,0 +1,44 @@ +#include "server.h" +#include "http.h" +#include "handlers.h" +#include +#include +#include + +void *handle_conn(void *arg) { + int fd = *(int*)arg; free(arg); + Req req; + if (parse_request(fd, &req) < 0) { close(fd); return NULL; } + + if (strcmp(req.method, "GET") == 0) { + if (!strcmp(req.path,"/") || !strcmp(req.path,"")) handle_index(fd); + else if (!strcmp(req.path,"/iptv.css")) send_css(fd); + else if (!strcmp(req.path,"/series")) handle_series_cats(fd); + else if (!strcmp(req.path,"/series/cat")) handle_series_list(fd, req.query); + else if (!strcmp(req.path,"/series/show")) handle_series_show(fd, req.query); + else if (!strcmp(req.path,"/movies")) handle_movie_cats(fd); + else if (!strcmp(req.path,"/movies/cat")) handle_movie_list(fd, req.query); + else if (!strcmp(req.path,"/search")) handle_search(fd, req.query); + else if (!strcmp(req.path,"/downloads")) handle_downloads(fd); + else if (!strcmp(req.path,"/api/downloads")) handle_api_downloads(fd); + else if (!strcmp(req.path,"/api/notifications")) handle_api_notifications(fd); + else { + /* try static JS files */ + send_static_js(fd, req.path); + } + } else if (strcmp(req.method, "POST") == 0) { + if (!strcmp(req.path,"/api/download")) handle_api_download(fd, req.body); + else if (!strcmp(req.path,"/api/download_movie")) handle_api_download_movie(fd, req.body); + else if (!strcmp(req.path,"/api/cancel")) handle_api_cancel(fd, req.body); + else if (!strcmp(req.path,"/api/clear")) handle_api_clear(fd); + else if (!strcmp(req.path,"/api/clear-cancelled")) handle_api_clear_cancelled(fd); + else if (!strcmp(req.path,"/api/retry-interrupted")) handle_api_retry_interrupted(fd); + else if (!strcmp(req.path,"/api/clean-partials")) handle_api_clean_partials(fd); + else if (!strcmp(req.path,"/api/notifications/test")) handle_api_notifications_test(fd); + else if (!strcmp(req.path,"/api/notifications/dismiss")) handle_api_notifications_dismiss(fd, req.body); + else send_404(fd); + } else send_404(fd); + + close(fd); + return NULL; +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..87fccc2 --- /dev/null +++ b/server.h @@ -0,0 +1,2 @@ +#pragma once +void *handle_conn(void *arg); diff --git a/server.o b/server.o new file mode 100644 index 0000000000000000000000000000000000000000..d53674ef2510879b297a1aa57a78a783bc7282ea GIT binary patch literal 4792 zcmb<-^>JfjWMqH=Mg}_u1P><4z+l0TU^{@B4h(z@ybQq}jc*iK85vSMx@A-~7#J8l zI_IcJure}q%fA3A^XPomc^}MbJy0RrXrtop8p2TG*l42?;2#{q-x9*ezyMVMr98S@ zzy_pvXtRJ#PJSZPqDv6L#DYxyl>Bn2;B|yxUVe#(v5N|a z$F9t@%;dz9%zO{y5ETIr&I|l47eUH;OH??Zf%WqEzyJS1;Q`}shw>Y0R9G1JTNnQO z|KDSWKB`?2j9{O_1R-{T9GDWHi)2?KLRoTR9@L#SkN^ScP0mS$3dW-fLJUkS(oIgx zOHM^N-U4A@Q7X)jMU}dlc_pbu2xSuy%0N~?P0Q0QNGvMJL@1L&Sc)&i^odf|TJaYY zP~gzcE7k{vzp%%yl+5DX%;I7OkfqX~@NW$SDeK*$!odnpwl7`&{{NrGFW&-6?hGC~ zAaR`nP6B-Vt&>49fTly}FDTL)KpGlrR3yqWQyKVMCt^|f@(Tq#yc&Y@R@JVhIC(s{zcZh#dBECX#un#rnk=`Q>1D^76NeVNsriFxO*8 zQBi3@3DkaZ{?;fY`<)Q#K>1Rjn7_5-55jrMU};GBG}fpHB&X&u@V9P*S%?&rKYzpg z3-j9Z-yp9!Ff`Vv@TVpgG4Que{);gAB!>F!2=x+)ImHb8tv8U=&;0%WKiCD}5c2)= z|Gx()YLG&x{5L2aO)E< z1}F_v04|t7MGiQt7c;<01!QhsehIA5$tz|6715~5PW^+)~fyGc91c?+Z;;@X$;O^{frJ&*N7pkdXXr^bV zXQ*IgU}R=sVr&4)YzzzxEDQ_`3JeSktPBhc?tY;RObm<%Kw_{uj}eV80@cS@6~w?; zA;2ij!_F~*k%2*lfq_8=Dwhm0%#lx^naQ1RLl+BE1{a@zGarX5H&h-Q1+tHY;p2a7 za?Dt2U@is*1|cMIu$>GH3}Orn3<6jTk;Wmei9_57hqwg}ad#ZzAvnZKafo-~5MPW# zd>;<+i#Wue;1CBjd9Z~W7bEuYl))jck3-x6hjtO+ItEsl^> zgG!X5cu3I%sym@lkk(*)PG)fl%p9l?xcr2w0$B$xsp7#66lh5W_DFIu+zTL85DU3qlb~n@VGtXP^@=NVOA?b9^omQ0Aan+dRg{{Os8^C& zQNo~?l30?+pqEr!%%E4458{B78|oP{=q2ap=BDPAFzDswm!#^s`-SR)(>s)(o>!_@ zP?TSgT2xZWpa(WEH6uQ)C^0t`Y&*pS$X}o&4ayV9hC%w)3=FXP9VQ1#OE5Nw28Ago zs$l9tVjye)O4JMt35HkR;H!44^U)oBkb8_g5q71u@a}0{|inP;&qP literal 0 HcmV?d00001 diff --git a/static/downloads.js b/static/downloads.js new file mode 100644 index 0000000..ab4169f --- /dev/null +++ b/static/downloads.js @@ -0,0 +1,49 @@ +/* ── Downloads page ─────────────────────────────────────────── */ +function escH(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } +function fmtSpeed(b) { return b>1048576?(b/1048576).toFixed(1)+' MB/s':b>1024?(b/1024).toFixed(0)+' KB/s':b+' B/s'; } +function fmtEta(s) { + if (s < 0) return ''; + if (s < 60) return s + 's'; + if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's'; + return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm'; +} + +function renderDownloads() { + fetch(BASE + '/api/downloads', {cache: 'no-store'}) + .then(function(r) { return r.json(); }) + .then(function(dl) { + var el = document.getElementById('dl-table'); + if (!dl.length) { el.innerHTML = '

No downloads yet.

'; return; } + var html = ''; + dl.slice().reverse().forEach(function(d) { + var pct = d.pct || 0, mb = (d.downloaded/1048576).toFixed(1); + var cls = d.status==='done' ? 'done' : d.status.startsWith('error') ? 'err' : d.status==='downloading' ? 'dl' : ''; + var thumb = d.cover_url + ? '' + : ''; + html += ''; + html += ''; + }); + html += '
NameStatusProgressSpeedETA
' + thumb + '' + escH(d.name) + '' + escH(d.status) + ''; + if (d.status === 'downloading') + html += '
 ' + pct + '% (' + mb + 'MB)'; + else html += pct + '%'; + html += '
' + (d.status==='downloading'&&d.speed_bps>0 ? fmtSpeed(d.speed_bps) : '—') + '' + (d.status==='downloading'&&d.eta_s>=0 ? fmtEta(d.eta_s) : '—') + ''; + if (d.status==='downloading' || d.status==='queued') + html += ''; + html += '
'; + el.innerHTML = html; + }).catch(function() {}); +} + +function cancelDl(id) { + fetch(BASE + '/api/cancel', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id:id})}) + .then(function() { setTimeout(renderDownloads, 400); }); +} +function clearDl() { fetch(BASE + '/api/clear', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } +function clearCancelled() { fetch(BASE + '/api/clear-cancelled', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } +function retryInterrupted() { fetch(BASE + '/api/retry-interrupted', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } +function cleanPartials() { fetch(BASE + '/api/clean-partials', {method:'POST'}).then(function() { renderDownloads(); }); } + +renderDownloads(); +setInterval(renderDownloads, 3000); diff --git a/static/footer.html b/static/footer.html new file mode 100644 index 0000000..9943ff0 --- /dev/null +++ b/static/footer.html @@ -0,0 +1,3 @@ +
+ + diff --git a/static/header.html b/static/header.html new file mode 100644 index 0000000..c7a300b --- /dev/null +++ b/static/header.html @@ -0,0 +1,17 @@ + + + + + +{{TITLE}} — IPTV + + + + +
+ diff --git a/static/iptv.css b/static/iptv.css new file mode 100644 index 0000000..7ef47be --- /dev/null +++ b/static/iptv.css @@ -0,0 +1,41 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&display=swap'); +:root{--black:#000;--offblack:#0a0a0a;--terminal:#ccc;--dim:#555;--border:#171717;--strawberry:#e8547a;--green:#4ade80} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--black);color:var(--terminal);font-family:'JetBrains Mono',monospace;min-height:100vh;padding:24px 16px 48px;-webkit-font-smoothing:antialiased} +header{border-bottom:1px solid var(--border);padding-bottom:20px;margin-bottom:24px;display:flex;align-items:baseline;gap:16px} +header h1{font-size:22px;font-weight:700;letter-spacing:-.04em;color:#fff} +nav{display:flex;gap:6px;flex-wrap:wrap} +nav a{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);text-decoration:none;padding:4px 10px;border:1px solid var(--border);border-radius:6px;transition:border-color .15s,color .15s} +nav a:hover{border-color:var(--strawberry);color:var(--strawberry)} +.c{max-width:960px;margin:0 auto} +.lbl{font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin-bottom:12px} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-bottom:28px} +.card{background:var(--offblack);border:1px solid var(--border);border-radius:12px;padding:14px 12px 10px;cursor:pointer;text-decoration:none;display:block;color:var(--terminal);transition:border-color .15s} +.card:hover{border-color:#2a2a2a} +.card:active{background:#0f0f0f} +.card-img{width:100%;aspect-ratio:2/3;background:var(--border);border-radius:6px;overflow:hidden;margin-bottom:8px} +.card-img img{width:100%;height:100%;object-fit:cover;display:block} +.card-name{font-size:12px;font-weight:700;color:#fff;letter-spacing:-.01em;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.card-sub{font-size:10px;color:var(--dim);letter-spacing:.04em} +table{width:100%;border-collapse:collapse;font-size:12px} +th{text-align:left;padding:8px 10px;background:var(--offblack);border-bottom:1px solid var(--border);font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);position:sticky;top:0} +td{padding:8px 10px;border-bottom:1px solid var(--border);vertical-align:middle} +tr:hover td{background:var(--offblack)} +.btn{display:inline-block;padding:4px 12px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:10px;font-family:inherit;letter-spacing:.06em;text-transform:uppercase;text-decoration:none;transition:border-color .15s,color .15s;background:transparent;color:var(--terminal)} +.btn:hover{border-color:var(--strawberry);color:var(--strawberry)} +.btn-g{border-color:var(--green);color:var(--green)}.btn-g:hover{border-color:#22d360;color:#22d360} +.btn-s{border-color:var(--strawberry);color:var(--strawberry)}.btn-r{border-color:#ef4444;color:#ef4444}.btn-r:hover{border-color:#f87171;color:#f87171} +input[type=checkbox]{cursor:pointer;accent-color:var(--strawberry)} +.search{width:100%;padding:8px 12px;background:var(--offblack);border:1px solid var(--border);color:var(--terminal);border-radius:8px;font-size:12px;font-family:inherit;margin-bottom:14px;outline:none;transition:border-color .15s} +.search:focus{border-color:#2a2a2a} +select{background:var(--offblack);border:1px solid var(--border);color:var(--terminal);border-radius:8px;font-size:12px;font-family:inherit;padding:8px 12px;outline:none;cursor:pointer} +.pbar{background:var(--border);border-radius:2px;height:2px;overflow:hidden;min-width:80px;display:inline-block;vertical-align:middle} +.pfill{background:var(--green);height:100%} +.done{color:var(--green)}.err{color:var(--strawberry)}.dl{color:#fbbf24} +.bc{font-size:10px;color:var(--dim);letter-spacing:.04em;margin-bottom:16px} +.bc a{color:var(--strawberry);text-decoration:none} +.sep{border:none;border-top:1px solid var(--border);margin:20px 0} +.sform{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap} +.sform input{flex:1;min-width:160px;margin-bottom:0} +.sform select{margin-bottom:0} +@media(min-width:480px){.grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr))}} diff --git a/static/iptv.js b/static/iptv.js new file mode 100644 index 0000000..8c31207 --- /dev/null +++ b/static/iptv.js @@ -0,0 +1,20 @@ +/* ── Shared IPTV downloader JS ──────────────────────────────── */ +var BASE = (function(){ + var m = window.location.pathname.match(/^(\/[^/]+)/); + return m ? m[1] : ''; +})(); + +function filter(el) { + document.querySelectorAll('.card').forEach(function(c) { + c.style.display = c.textContent.toLowerCase().includes(el.value.toLowerCase()) ? '' : 'none'; + }); +} + +function dlMov(id, title, ext, icon) { + if (!confirm('Download: ' + title + '?')) return; + fetch(BASE + '/api/download_movie', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({id: id, title: title, ext: ext || 'mp4', cover_url: icon || ''}) + }).then(function() { alert('Queued!'); location.href = BASE + '/downloads'; }); +} diff --git a/static/series_show.js b/static/series_show.js new file mode 100644 index 0000000..6dad3e1 --- /dev/null +++ b/static/series_show.js @@ -0,0 +1,65 @@ +/* ── Series episode selector ──────────────────────────────────── */ +/* TITLE, COVER, SEASONS injected by server as window vars before this script */ + +var tabs = document.getElementById('tabs'); +var sdiv = document.getElementById('seasons'); + +Object.keys(SEASONS).sort(function(a,b){return +a-+b;}).forEach(function(s) { + var btn = document.createElement('button'); + btn.className = 'btn b'; btn.textContent = 'S' + s.padStart(2,'0'); + btn.style.marginRight = '6px'; + btn.onclick = function() { showSeason(s); }; + tabs.appendChild(btn); + + var div = document.createElement('div'); div.id = 's-' + s; div.style.display = 'none'; + var html = '
' + + ' ' + + '
'; + html += '' + + ''; + SEASONS[s].forEach(function(e) { + var epn = e.episode_num || e.episodeNum || '?'; + var dur = (e.info && e.info.duration) || ''; + var ext = e.container_extension || 'mp4'; + html += '' + + '' + + '' + + '' + + ''; + }); + html += '
EpTitleDuration
E' + String(epn).padStart(2,'0') + '' + (e.title||'') + '' + dur + '
'; + div.innerHTML = html; + sdiv.appendChild(div); +}); + +var firstS = Object.keys(SEASONS).sort(function(a,b){return +a-+b;})[0]; +if (firstS) showSeason(firstS); + +function showSeason(s) { + document.querySelectorAll('[id^=s-]').forEach(function(d) { d.style.display = 'none'; }); + var d = document.getElementById('s-' + s); if (d) d.style.display = ''; +} + +function selAll(s, v) { + var all = Array.from(document.querySelectorAll('.s-cb-' + s)); + var checked = v === undefined ? !all.every(function(x){return x.checked;}) : v; + all.forEach(function(c) { c.checked = checked; }); +} + +function dlSeason(s) { selAll(s, true); dlSelected(); } + +function dlSelected() { + var eps = Array.from(document.querySelectorAll('.ep-cb:checked')).map(function(c) { + return {id: c.dataset.id, ep: c.dataset.ep, s: c.dataset.s, ext: c.dataset.ext}; + }); + if (!eps.length) { alert('Select episodes first'); return; } + var bySeason = {}; + eps.forEach(function(e) { (bySeason[e.s] = bySeason[e.s]||[]).push({id:e.id, episode_num:e.ep, ext:e.ext}); }); + Promise.all(Object.keys(bySeason).map(function(s) { + return fetch(BASE + '/api/download', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({episodes: bySeason[s], series_title: TITLE, season: s, cover_url: COVER}) + }).then(function(r) { return r.json(); }); + })).then(function() { alert('Queued!'); location.href = BASE + '/downloads'; }); +}