fix: inject <base href> from base_path config; fix relative asset/nav links; add README

This commit is contained in:
2026-06-09 01:46:53 +02:00
parent 9fc9494ccd
commit 6e40fc0eb1
6 changed files with 123 additions and 9 deletions
+100
View File
@@ -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
+2
View File
@@ -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);
+1
View File
@@ -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 */
+2 -2
View File
@@ -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) {
"<button class='btn btn-r' onclick=cleanPartials()>clean partial files</button>"
"</div>"
"<div id=dl-table><p style='color:var(--dim)'>loading...</p></div>");
send_page(fd, "Downloads", &b, "/downloads.js");
send_page(fd, "Downloads", &b, "downloads.js");
buf_free(&b);
}
+12 -1
View File
@@ -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 <base href> 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, "</head>");
if (ph && head_end && head_end > ph) {
/* write up to </head>, 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, "<base href=\"%s/\">", 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);
+6 -6
View File
@@ -4,14 +4,14 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{TITLE}} — IPTV</title>
<link rel="stylesheet" href="/iptv.css">
<link rel="stylesheet" href="iptv.css">
</head>
<body>
<nav class="topnav">
<a href="/series" class="nav-item">Series</a>
<a href="/movies" class="nav-item">Movies</a>
<a href="/search" class="nav-item">Search</a>
<a href="/downloads" class="nav-item">Downloads</a>
<a href="series" class="nav-item">Series</a>
<a href="movies" class="nav-item">Movies</a>
<a href="search" class="nav-item">Search</a>
<a href="downloads" class="nav-item">Downloads</a>
</nav>
<div class="container">
<script src="/iptv.js"></script>
<script src="iptv.js"></script>