refactor: reorganize into subdirs (config/ util/ http/ notifs/ queue/ integrations/ static/)
This commit is contained in:
+454
@@ -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}");
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
void *handle_conn(void *arg);
|
||||
Reference in New Issue
Block a user