diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b52781 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# iptv-downloader + +A lightweight HTTP server for browsing and downloading content from an Xtream Codes IPTV provider. Written in C, zero runtime dependencies beyond libcurl and pthreads. + +## Features + +- Browse series and movies by category +- Full-text search across the entire catalogue +- Sequential download queue with progress tracking, speed and ETA +- Resume interrupted downloads; skip already-downloaded files +- Discord webhook notifications on completion or failure +- Jellyfin library refresh triggered after each successful download +- Notification history with dismiss support +- Runtime configuration — no recompile needed to change provider or paths + +## Requirements + +- GCC, make +- libcurl (development headers) +- Linux (uses `SO_BINDTODEVICE` for VPN binding) + +## Build + +```sh +make +make test # optional — runs unit tests +``` + +## Install + +```sh +sudo make install +# installs binary to /usr/local/bin/iptv-dl +# installs static files to /usr/local/share/iptv-dl/{css,js,html}/ +``` + +## Configuration + +Create a config file at one of the following locations (first found wins): + +1. Path passed via `--config PATH` +2. `$IPTV_DL_CONFIG` environment variable +3. `~/.iptv-downloader/config.json` +4. `/etc/iptv-downloader/config.json` +5. Built-in defaults (runs without any config file) + +Example config: + +```json +{ + "port": 8787, + "bind_iface": "wg0-mullvad", + "iptv_api": "http://your.provider/player_api.php", + "iptv_user": "username", + "iptv_pass": "password", + "stream_base": "http://your.provider:80", + "dl_dir_tv": "/mnt/media/TV", + "dl_dir_mov": "/mnt/media/Movies", + "discord_webhook": "", + "jellyfin_url": "", + "jellyfin_token": "", + "template_dir": "/usr/local/share/iptv-dl", + "data_dir": "/var/lib/iptv-dl", + "max_recv_speed": 20971520 +} +``` + +Print the active config: + +```sh +iptv-dl --dump-config +``` + +## Source layout + +``` +config/ Runtime configuration (load/save/defaults) +util/ String buffer, JSON parser, URL/HTML/JS escaping +http/ HTTP request parsing, response helpers, route handlers +notifs/ Notification ring buffer with persistence +queue/ Download queue, worker thread, history, Xtream Codes API client +integrations/ Discord webhook, Jellyfin library refresh +static/css/ Stylesheet +static/js/ Shared JS (BASE, filter, dlMov), downloads page, series selector +static/html/ Page header and footer templates +tests/ Unit tests +main.c Server entry point (~80 lines) +``` + +## Usage + +```sh +iptv-dl [--config PATH] [--dump-config] +``` + +The web UI is served at `http://localhost:8787/`. If running behind a reverse proxy at a sub-path (e.g. `/iptv/`), all asset and navigation links are relative and work correctly. + +## License + +MIT diff --git a/config/config.c b/config/config.c index 0ed9fac..83a3cdd 100644 --- a/config/config.c +++ b/config/config.c @@ -19,6 +19,7 @@ static void set_defaults(void) { 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); + strncpy(g_cfg.base_path, "", sizeof(g_cfg.base_path)-1); g_cfg.max_recv_speed = 20L * 1024 * 1024; /* 20 MiB/s */ } @@ -79,6 +80,7 @@ static int try_load(const char *path) { 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)); + cfg_read_str(buf, "base_path", g_cfg.base_path, sizeof(g_cfg.base_path)); 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); diff --git a/config/config.h b/config/config.h index 0eff778..06c1a91 100644 --- a/config/config.h +++ b/config/config.h @@ -32,6 +32,7 @@ typedef struct { /* Paths */ char template_dir[512]; /* dir containing header.html, footer.html, iptv.css, *.js */ char data_dir[512]; /* dir for history.json, notifications.json */ + char base_path[128]; /* URL prefix when behind reverse proxy, e.g. "/iptv" (no trailing slash) */ /* Limits */ long max_recv_speed; /* bytes/sec, 0 = unlimited */ diff --git a/http/handlers.c b/http/handlers.c index 587fa32..49e1aca 100644 --- a/http/handlers.c +++ b/http/handlers.c @@ -117,7 +117,7 @@ void handle_series_show(int fd, const char *qs) { buf_free(&inl); free(sid); free(title_raw); free(cover_raw); free(json); - send_page(fd, "Series", &b, "/series_show.js"); + send_page(fd, "Series", &b, "series_show.js"); buf_free(&b); } @@ -226,7 +226,7 @@ void handle_downloads(int fd) { "" "" "

loading...

"); - send_page(fd, "Downloads", &b, "/downloads.js"); + send_page(fd, "Downloads", &b, "downloads.js"); buf_free(&b); } diff --git a/http/http.c b/http/http.c index 9c9a4b0..cd63e34 100644 --- a/http/http.c +++ b/http/http.c @@ -77,8 +77,19 @@ int parse_request(int fd, Req *req) { 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"); + /* inject so relative assets resolve correctly at any sub-path */ const char *ph = strstr(g_header, "{{TITLE}}"); - if (ph) { + const char *head_end = strstr(g_header, ""); + if (ph && head_end && head_end > ph) { + /* write up to , inject base tag, then rest of header */ + buf_append(&page, g_header, ph - g_header); + buf_str(&page, title); + const char *after_title = ph + 9; + buf_append(&page, after_title, head_end - after_title); + if (g_cfg.base_path[0]) + buf_fmt(&page, "", g_cfg.base_path); + buf_str(&page, head_end); + } else if (ph) { buf_append(&page, g_header, ph - g_header); buf_str(&page, title); buf_str(&page, ph + 9); diff --git a/static/html/header.html b/static/html/header.html index c7a300b..de99d01 100644 --- a/static/html/header.html +++ b/static/html/header.html @@ -4,14 +4,14 @@ {{TITLE}} — IPTV - +
- +