refactor: reorganize into subdirs (config/ util/ http/ notifs/ queue/ integrations/ static/)

This commit is contained in:
2026-06-09 00:56:27 +02:00
parent 4bcc34ec86
commit 32deda57e0
23 changed files with 33 additions and 18 deletions
+454
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/stat.h>
/* ── 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, "<p class=lbl>iptv downloader</p>"
"<p style='font-size:12px;color:var(--dim)'>"
"Browse <a href='series' style='color:var(--strawberry)'>series</a>, "
"<a href='movies' style='color:var(--strawberry)'>movies</a>, or "
"<a href='search' style='color:var(--strawberry)'>search by name</a>."
"</p>");
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, "<p class=lbl>series categories</p>"
"<input class=search placeholder='filter...' oninput=filter(this)>"
"<div class=grid id=g>");
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, "<a class=card href='series/cat?id=%s'><div class=card-name>", id?id:"");
html_esc(&b, name?name:"?"); buf_str(&b, "</div></a>");
free(id); free(name); free(arr[i]);
}
buf_str(&b, "</div>"); 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, "<p class=bc><a href='series'>series</a> / category</p>"
"<p class=lbl>series — %d results</p>", n);
buf_str(&b, "<input class=search placeholder='Filter...' oninput=filter(this)><div class=grid id=g>");
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, "<a class=card href='series/show?id=%s'>", sid?sid:"");
if (cover && cover[0]) {
buf_str(&b,"<div class=card-img><img src='"); html_esc(&b, cover);
buf_str(&b,"' loading=lazy onerror=\"this.parentNode.style.display='none'\"></div>");
}
buf_str(&b,"<div class=card-name>"); html_esc(&b, name?name:"?"); buf_str(&b,"</div></a>");
free(sid); free(name); free(cover); free(arr[i]);
}
buf_str(&b, "</div>"); 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, "<p class=bc><a href='series'>series</a> / "); html_esc(&b, title);
buf_str(&b, "</p><p class=lbl>"); html_esc(&b, title); buf_str(&b, "</p>");
buf_str(&b, "<div id=tabs style='margin-bottom:10px'></div>");
buf_str(&b, "<div style='margin-bottom:10px'>"
"<button class='btn btn-g' onclick=dlSelected()>⬇ Download Selected</button>"
"</div>");
buf_str(&b, "<div id=seasons></div>");
/* Inject TITLE, COVER, SEASONS as window vars before the external script */
Buf inl; buf_init(&inl);
buf_str(&inl, "<script>");
buf_str(&inl, "var TITLE='"); js_esc(&inl, title); buf_str(&inl, "';\n");
buf_str(&inl, "var COVER='"); if (cover_raw) js_esc(&inl, cover_raw); buf_str(&inl, "';\n");
buf_str(&inl, "var SEASONS=");
if (eps_start) {
eps_start += strlen("\"episodes\":");
int depth = 0; const char *p = eps_start; const char *obj_end = NULL;
for (; *p; p++) { if(*p=='{') depth++; else if(*p=='}'){depth--;if(depth==0){obj_end=p+1;break;}} }
if (obj_end) buf_append(&inl, eps_start, obj_end-eps_start); else buf_str(&inl, "{}");
} else buf_str(&inl, "{}");
buf_str(&inl, ";</script>");
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, "<p class=lbl>movie categories</p>"
"<input class=search placeholder='filter...' oninput=filter(this)>"
"<div class=grid id=g>");
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, "<a class=card href='movies/cat?id=%s'><div class=card-name>", id?id:"");
html_esc(&b, name?name:"?"); buf_str(&b, "</div></a>");
free(id); free(name); free(arr[i]);
}
buf_str(&b, "</div>"); 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, "<p class=bc><a href='movies'>movies</a> / category</p>"
"<p class=lbl>movies — %d results</p>", n);
buf_str(&b, "<input class=search placeholder='Filter...' oninput=filter(this)><div class=grid id=g>");
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, "<a class=card href='#' onclick=\"dlMov('%s','", vid?vid:"");
js_esc(&b, name?name:"?");
buf_fmt(&b, "','%s','", ext?ext:"mp4");
js_esc(&b, icon?icon:"");
buf_str(&b, "');return false\">");
if (icon && icon[0]) {
buf_str(&b,"<div class=card-img><img src='"); html_esc(&b,icon);
buf_str(&b,"' loading=lazy onerror=\"this.parentNode.style.display='none'\"></div>");
}
buf_str(&b, "<div class=card-name>"); html_esc(&b, name?name:"?"); buf_str(&b, "</div></a>");
free(vid); free(name); free(ext); free(icon); free(arr[i]);
}
buf_str(&b, "</div>"); 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, "<p class=lbl>search</p><form method=get action='search' class=sform>");
buf_str(&b, "<input class=search name=q placeholder='name...' value='");
if (q) html_esc(&b, q);
buf_fmt(&b, "' style='margin-bottom:0'>"
"<select name=type><option value=series%s>series</option>"
"<option value=movies%s>movies</option></select>",
is_series?" selected":"", !is_series?" selected":"");
buf_str(&b, "<button type=submit class='btn btn-s'>go</button></form>");
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, "<div class=grid id=g>");
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, "<a class=card href='series/show?id=%s'>", sid?sid:"");
if (cover && cover[0]) { buf_str(&b,"<div class=card-img><img src='"); html_esc(&b,cover); buf_str(&b,"' loading=lazy onerror=\"this.parentNode.style.display='none'\"></div>"); }
buf_str(&b,"<div class=card-name>"); html_esc(&b,name); buf_str(&b,"</div></a>");
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,"<a class=card href='#' onclick=\"dlMov('%s','",vid?vid:""); js_esc(&b,name);
buf_fmt(&b,"','%s','",ext?ext:"mp4"); js_esc(&b,icon?icon:""); buf_str(&b,"');return false\">");
if (icon&&icon[0]){buf_str(&b,"<div class=card-img><img src='");html_esc(&b,icon);buf_str(&b,"' loading=lazy onerror=\"this.parentNode.style.display='none'\"></div>");}
buf_str(&b,"<div class=card-name>"); html_esc(&b,name); buf_str(&b,"</div></a>");
free(vid); free(ext); free(icon);
}
free(name); free(arr[i]); count++;
}
buf_str(&b, "</div>"); free(arr); free(json);
char cnt[64]; snprintf(cnt,sizeof(cnt),"<p class=lbl style='margin-bottom:12px'>%d results</p>",count);
Buf out; buf_init(&out);
const char *gp = strstr(b.data,"<div class=grid");
if (gp) { buf_append(&out,b.data,gp-b.data); buf_str(&out,cnt); buf_str(&out,gp); }
else { buf_str(&out,cnt); buf_append(&out,b.data,b.len); }
send_page(fd, "Search", &out, NULL);
buf_free(&b); buf_free(&out); free(q); free(type);
}
void handle_downloads(int fd) {
Buf b; buf_init(&b);
buf_str(&b, "<p class=lbl>downloads</p>"
"<div style='margin-bottom:16px;display:flex;gap:8px;flex-wrap:wrap'>"
"<button class='btn' onclick=clearDl()>clear all finished</button>"
"<button class='btn' onclick=clearCancelled()>clear cancelled</button>"
"<button class='btn btn-g' onclick=retryInterrupted()>retry interrupted</button>"
"<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");
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}");
}
+24
View File
@@ -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);
+144
View File
@@ -0,0 +1,144 @@
#include "http.h"
#include "config.h" /* g_cfg */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
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=\"%s\"></script>", 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));
}
+31
View File
@@ -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);
+44
View File
@@ -0,0 +1,44 @@
#include "server.h"
#include "http.h"
#include "handlers.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}
+2
View File
@@ -0,0 +1,2 @@
#pragma once
void *handle_conn(void *arg);