fix: inject <base href> from base_path config; fix relative asset/nav links; add README
This commit is contained in:
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user