#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 #include #include #include #include #include #include /* ── 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, "

iptv downloader

" "

" "Browse series, " "movies, or " "search by name." "

"); 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, "

series categories

" "" "
"); 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, "
", id?id:""); html_esc(&b, name?name:"?"); buf_str(&b, "
"); free(id); free(name); free(arr[i]); } buf_str(&b, "
"); 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, "

series / category

" "

series — %d results

", n); buf_str(&b, "
"); 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, "", sid?sid:""); if (cover && cover[0]) { buf_str(&b,"
"); } buf_str(&b,"
"); html_esc(&b, name?name:"?"); buf_str(&b,"
"); free(sid); free(name); free(cover); free(arr[i]); } buf_str(&b, "
"); 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, "

series / "); html_esc(&b, title); buf_str(&b, "

"); html_esc(&b, title); buf_str(&b, "

"); buf_str(&b, "
"); buf_str(&b, "
" "" "
"); buf_str(&b, "
"); /* Inject TITLE, COVER, SEASONS as window vars before the external script */ Buf inl; buf_init(&inl); buf_str(&inl, ""); 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, "

movie categories

" "" "
"); 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, "
", id?id:""); html_esc(&b, name?name:"?"); buf_str(&b, "
"); free(id); free(name); free(arr[i]); } buf_str(&b, "
"); 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, "

movies / category

" "

movies — %d results

", n); buf_str(&b, "
"); 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, ""); if (icon && icon[0]) { buf_str(&b,"
"); } buf_str(&b, "
"); html_esc(&b, name?name:"?"); buf_str(&b, "
"); free(vid); free(name); free(ext); free(icon); free(arr[i]); } buf_str(&b, "
"); 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, "

search

"); buf_str(&b, "" "", is_series?" selected":"", !is_series?" selected":""); buf_str(&b, "
"); 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, "
"); 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, "", sid?sid:""); if (cover && cover[0]) { buf_str(&b,"
"); } buf_str(&b,"
"); html_esc(&b,name); buf_str(&b,"
"); 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,""); if (icon&&icon[0]){buf_str(&b,"
");} buf_str(&b,"
"); html_esc(&b,name); buf_str(&b,"
"); free(vid); free(ext); free(icon); } free(name); free(arr[i]); count++; } buf_str(&b, "
"); free(arr); free(json); char cnt[64]; snprintf(cnt,sizeof(cnt),"

%d results

",count); Buf out; buf_init(&out); const char *gp = strstr(b.data,"
downloads

" "
" "" "" "" "" "
" "

loading...

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