Files
iptv-downloader/http/handlers.c
T

455 lines
20 KiB
C

#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}");
}