export default { async fetch(request, env, ctx) { const { pathname, searchParams } = new URL(request.url); const db = env.DB; const kv = env.KV_CONNECTIONS; try { await db.prepare("ALTER TABLE settings ADD COLUMN tmdb_api_key TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE settings ADD COLUMN dashboard_links TEXT DEFAULT ''").run(); } catch (e) {} const settings = await db .prepare("SELECT admin_user, admin_pass, tmdb_api_key, dashboard_links FROM settings WHERE id = 1") .first(); const ADMIN_USER = settings?.admin_user || "admin"; const ADMIN_PASS = settings?.admin_pass || "SecretPassword123"; const hostUrl = new URL(request.url).origin; // Auto-add optional stream header columns if this D1 database was created before this update. // Safe to leave in place: existing-column errors are ignored. async function ensureStreamHeaderColumns() { try { await db.prepare("ALTER TABLE streams ADD COLUMN user_agent TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN referer TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN content_type TEXT DEFAULT 'live'").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN image_url TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_id TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_type TEXT DEFAULT 'movie'").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_poster_url TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_backdrop_url TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_overview TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_year TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE streams ADD COLUMN tmdb_rating TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE settings ADD COLUMN tmdb_api_key TEXT DEFAULT ''").run(); } catch (e) {} try { await db.prepare("ALTER TABLE settings ADD COLUMN dashboard_links TEXT DEFAULT ''").run(); } catch (e) {} } await ensureStreamHeaderColumns(); const TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"; const getTmdbKey = () => env.TMDB_API_KEY || settings?.tmdb_api_key || ""; const tmdbImageUrl = (path) => path ? `${TMDB_IMAGE_BASE}${path}` : ""; const cleanVodTitle = (title) => String(title || "") .replace(/\[[^\]]*\]|\([^)]*\)/g, " ") .replace(/\b(19|20)\d{2}\b/g, " ") .replace(/\b(1080p|720p|2160p|4k|uhd|hdr|bluray|web[- ]?dl|x264|x265|hevc|aac|multi|proper|repack)\b/gi, " ") .replace(/[._-]+/g, " ") .replace(/\s+/g, " ") .trim(); const splitCategoryImage = (category = "") => ({ category: String(category || "").trim(), image_url: "" }); const parseM3uAttributes = (line = "") => { const attrs = {}; const re = /([A-Za-z0-9_-]+)="([^"]*)"/g; let match; while ((match = re.exec(line)) !== null) { attrs[match[1].toLowerCase()] = match[2]; } return attrs; }; async function fetchTmdbMeta(title, mediaType = "movie") { const apiKey = getTmdbKey(); const query = cleanVodTitle(title); if (!apiKey || !query) return null; const type = mediaType === "tv" ? "tv" : "movie"; const tmdbUrl = `https://api.themoviedb.org/3/search/${type}?api_key=${encodeURIComponent(apiKey)}&query=${encodeURIComponent(query)}&include_adult=false`; const res = await fetch(tmdbUrl); if (!res.ok) return null; const data = await res.json(); const item = data.results?.[0]; if (!item) return null; return { tmdb_id: String(item.id || ""), tmdb_type: type, name: item.title || item.name || title, poster: tmdbImageUrl(item.poster_path), backdrop: tmdbImageUrl(item.backdrop_path), overview: item.overview || "", year: (item.release_date || item.first_air_date || "").slice(0, 4), rating: item.vote_average ? String(Math.round(item.vote_average * 10) / 10) : "" }; } async function buildVodPayload(body) { const useTmdb = body.tmdb_autofill !== false; const tmdbType = body.tmdb_type === "tv" ? "tv" : "movie"; const meta = useTmdb ? await fetchTmdbMeta(body.name, tmdbType) : null; const poster = meta?.poster || body.tmdb_poster_url || ""; const baseCategory = (body.category || "VOD").split("|")[0] || "VOD"; return { name: meta?.name || body.name, url: body.url, category: baseCategory || "VOD", user_agent: body.user_agent || "", referer: body.referer || "", tmdb_id: meta?.tmdb_id || body.tmdb_id || "", tmdb_type: meta?.tmdb_type || tmdbType, image_url: poster || body.image_url || "", tmdb_poster_url: poster, tmdb_backdrop_url: meta?.backdrop || body.tmdb_backdrop_url || "", tmdb_overview: meta?.overview || body.tmdb_overview || "", tmdb_year: meta?.year || body.tmdb_year || "", tmdb_rating: meta?.rating || body.tmdb_rating || "" }; } const isAccountExpired = (expDateStr) => { if (!expDateStr || expDateStr === "Never") return false; const expiry = new Date(expDateStr + "T23:59:59"); if (isNaN(expiry.getTime())) return false; return Date.now() > expiry.getTime(); }; if (pathname === "/proxy") { const encoded = searchParams.get("data"); const user = searchParams.get("user"); const pass = searchParams.get("pass"); if (!encoded) { return new Response("Missing Stream URL", { status: 400 }); } let streamUrl; try { streamUrl = atob(encoded); } catch { return new Response("Invalid Stream Token", { status: 400 }); } if (!streamUrl) return new Response("Missing Stream URL", { status: 400 }); if (!user || !pass) return new Response("Missing Credentials", { status: 401 }); const userCheck = await db.prepare("SELECT * FROM users WHERE username = ? AND password = ? AND status = 'active'").bind(user, pass).first(); if (!userCheck) return new Response("Unauthorized Line", { status: 401 }); if (isAccountExpired(userCheck.exp_date)) { return new Response("Subscription Expired. Access Denied.", { status: 403, headers: { "Access-Control-Allow-Origin": "*" } }); } const maxAllowed = parseInt(userCheck.max_connections) || 1; const kvKey = `active_conn:${user}`; let currentConns = 0; if (kv) { const stored = await kv.get(kvKey); currentConns = stored ? parseInt(stored) : 0; if (currentConns >= maxAllowed) { return new Response(`Connection Limit Reached (${currentConns}/${maxAllowed}). Close existing stream first.`, { status: 403, headers: { "Access-Control-Allow-Origin": "*" } }); } await kv.put(kvKey, (currentConns + 1).toString(), { expirationTtl: 14400 }); } try { const customUA = searchParams.get("ua"); const customReferer = searchParams.get("referer"); /** @type {Record} */ const proxyHeaders = {}; if (customUA) { proxyHeaders["User-Agent"] = customUA; } if (customReferer) { proxyHeaders["Referer"] = customReferer; } const response = await fetch(streamUrl, { headers: proxyHeaders }); const newHeaders = new Headers(response.headers); newHeaders.set("Access-Control-Allow-Origin", "*"); const originalBody = response.body; const transformStream = new TransformStream({ flush(controller) { if (kv) { ctx.waitUntil((async () => { const freshCount = await kv.get(kvKey); const currentVal = freshCount ? parseInt(freshCount) : 1; await kv.put(kvKey, Math.max(0, currentVal - 1).toString(), { expirationTtl: 14400 }); })()); } } }); const modifiedBody = originalBody.pipeThrough(transformStream); return new Response(modifiedBody, { status: response.status, headers: newHeaders }); } catch (e) { if (kv) { const freshCount = await kv.get(kvKey); const currentVal = freshCount ? parseInt(freshCount) : 1; await kv.put(kvKey, Math.max(0, currentVal - 1).toString(), { expirationTtl: 14400 }); } return new Response("Proxy Playback Error: " + e.message, { status: 500 }); } } if (pathname.startsWith("/play/")) { const user = searchParams.get("user"); const pass = searchParams.get("pass"); const streamId = pathname.split("/play/")[1]; const userCheck = await db.prepare( "SELECT * FROM users WHERE username = ? AND password = ? AND status='active'" ) .bind(user, pass) .first(); if (!userCheck) { return new Response("Unauthorized", { status: 401 }); } const stream = await db.prepare( "SELECT * FROM streams WHERE id = ?" ) .bind(streamId) .first(); if (!stream) { return new Response("Stream Not Found", { status: 404 }); } /** @type {Record} */ const playHeaders = {}; if (stream.user_agent) { playHeaders["User-Agent"] = stream.user_agent; } if (stream.referer) { playHeaders["Referer"] = stream.referer; } const response = await fetch(stream.url, { headers: playHeaders }); const playResponseHeaders = new Headers(response.headers); playResponseHeaders.set("Access-Control-Allow-Origin", "*"); return new Response(response.body, { status: response.status, headers: playResponseHeaders }); } if (pathname === "/get_playlist") { const user = searchParams.get("user"); const pass = searchParams.get("pass"); const proxyId = searchParams.get("proxy"); const includeVod = searchParams.get("include_vod") !== "0"; const userCheck = await db.prepare("SELECT * FROM users WHERE username = ? AND password = ? AND status = 'active'").bind(user, pass).first(); if (!userCheck) return new Response("Unauthorized Account", { status: 401 }); if (isAccountExpired(userCheck.exp_date)) { return new Response("Subscription Expired. Playlist generation locked.", { status: 403 }); } let baseProxyString = ""; let isBuiltIn = false; let isNoProxy = false; if (proxyId === 'none') { isNoProxy = true; } else if (proxyId === 'default' || !proxyId) { baseProxyString = `${hostUrl}/proxy?user=${encodeURIComponent(user)}&pass=${encodeURIComponent(pass)}&data=`; isBuiltIn = true; } else { const proxy = await db.prepare("SELECT url FROM proxies WHERE id = ?").bind(proxyId).first(); if (proxy) baseProxyString = proxy.url; } const streams = await db.prepare( includeVod ? "SELECT * FROM streams" : "SELECT * FROM streams WHERE COALESCE(content_type, 'live') != 'vod'" ).all(); const EPG_URL = "https://epgshare01.online/epgshare01/epg_ripper_ALL_SOURCES1.xml.gz"; let m3u = `#EXTM3U url-tvg="${EPG_URL}"\n`; for (const stream of streams.results) { let targetUrl = stream.url; if (!isNoProxy) { if (isBuiltIn) { const encodedUrl = btoa(stream.url); targetUrl = `${baseProxyString}${encodeURIComponent(encodedUrl)}`; if (stream.user_agent) { targetUrl += `&ua=${encodeURIComponent(stream.user_agent)}`; } if (stream.referer) { targetUrl += `&referer=${encodeURIComponent(stream.referer)}`; } } else if (baseProxyString) { let computedProxy = baseProxyString .replace(/{user}/g, encodeURIComponent(user)) .replace(/{pass}/g, encodeURIComponent(pass)) .replace(/{ua}/g, encodeURIComponent(stream.user_agent || "")) .replace(/{user_agent}/g, encodeURIComponent(stream.user_agent || "")) .replace(/{referer}/g, encodeURIComponent(stream.referer || "")); targetUrl = `${computedProxy}${stream.url}`; }} let category = stream.category || ""; let logo = stream.image_url || ""; const directTmdbLogo = stream.tmdb_poster_url || logo; const logoTag = directTmdbLogo ? `tvg-logo="${directTmdbLogo}"` : ""; const yearTag = stream.tmdb_year ? ` tvg-year="${stream.tmdb_year}"` : ""; const ratingTag = stream.tmdb_rating ? ` tvg-rating="${stream.tmdb_rating}"` : ""; m3u += `#EXTINF:-1 tvg-name="${stream.name}" ${logoTag}${yearTag}${ratingTag} group-title="${category}",${stream.name}\n`; if (stream.user_agent) { m3u += `#EXTVLCOPT:http-user-agent=${stream.user_agent}\n`; } if (stream.referer) { m3u += `#EXTVLCOPT:http-referrer=${stream.referer}\n`; } m3u += `${targetUrl}\n`; } return new Response(m3u, { headers: { "Content-Type": "application/mpegurl", "Content-Disposition": `attachment; filename="${user}_playlist.m3u"`, "Access-Control-Allow-Origin": "*" } }); } const COOKIE_NAME = "tfms_admin_session"; function getCookies(req) { const cookieHeader = req.headers.get("Cookie") || ""; return Object.fromEntries( cookieHeader.split(";").map(c => { const parts = c.trim().split("="); return [parts[0], parts[1]]; }) ); } const cookies = getCookies(request); if (pathname === "/login" && request.method === "GET") { return new Response(` TFMS Admin Login `, { headers: { "Content-Type": "text/html" } }); } if (pathname === "/login" && request.method === "POST") { const form = await request.formData(); const username = form.get("username"); const password = form.get("password"); if ( username === ADMIN_USER && password === ADMIN_PASS ) { return new Response(null, { status: 302, headers: { "Location": "/", "Set-Cookie": `${COOKIE_NAME}=authorized; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400` } }); } return Response.redirect(`${hostUrl}/login?error=1`, 302); } if (pathname === "/logout") { return new Response(null, { status: 302, headers: { "Location": "/login", "Set-Cookie": `${COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0` } }); } const publicRoutes = [ "/proxy", "/get_playlist", "/login" ]; const isPublic = publicRoutes.some(route => pathname.startsWith(route)); if (!isPublic) { if (cookies[COOKIE_NAME] !== "authorized") { return Response.redirect(`${hostUrl}/login`, 302); } } if (request.method === "POST" && pathname.startsWith("/api/")) { const body = await request.json(); if (pathname === "/api/tinyurl") { const longUrl = body.url; if (!longUrl || typeof longUrl !== "string") { return Response.json({ error: "Missing URL" }, { status: 400 }); } try { const tinyRes = await fetch( "https://tinyurl.com/api-create.php?url=" + encodeURIComponent(longUrl) ); if (!tinyRes.ok) { return Response.json({ error: "TinyURL request failed" }, { status: 500 }); } const tinyUrl = (await tinyRes.text()).trim(); if (!tinyUrl.startsWith("http")) { return Response.json({ error: tinyUrl || "TinyURL returned an invalid response" }, { status: 500 }); } return Response.json({ success: true, tinyUrl }); } catch (e) { return Response.json({ error: e.message }, { status: 500 }); } } if (pathname === "/api/settings/save") { await db.prepare(` UPDATE settings SET admin_user = ?, admin_pass = ?, tmdb_api_key = ? WHERE id = 1 `) .bind(body.admin_user, body.admin_pass, body.tmdb_api_key || "") .run(); return Response.json({ success: true }); } if (pathname === "/api/dashboard_links/save") { const links = Array.isArray(body.links) ? body.links : []; const cleanLinks = links.slice(0, 20).map(link => ({ name: String(link?.name || "Quick Link").trim().slice(0, 80) || "Quick Link", url: String(link?.url || "").trim().slice(0, 1000) })); await db.prepare(` UPDATE settings SET dashboard_links = ? WHERE id = 1 `) .bind(JSON.stringify(cleanLinks)) .run(); return Response.json({ success: true, links: cleanLinks }); } if (pathname === "/api/comments/save") { const content = body.content || ""; const exists = await db.prepare("SELECT id FROM comments WHERE id = 1").first(); if (exists) { await db.prepare( "UPDATE comments SET content = ?, updated_at = ? WHERE id = 1" ).bind(content, new Date().toISOString()).run(); } else { await db.prepare( "INSERT INTO comments (id, content, updated_at) VALUES (1, ?, ?)" ).bind(content, new Date().toISOString()).run(); } return Response.json({ success: true }); } if (pathname === "/api/tmdb/search") { const meta = await fetchTmdbMeta(body.title || body.name, body.tmdb_type || "movie"); if (!meta) return Response.json({ error: "TMDB match not found or API key missing" }, { status: 404 }); return Response.json({ success: true, meta }); } if (pathname === "/api/users/add") { await db.prepare("INSERT INTO users (username, password, exp_date, max_connections) VALUES (?, ?, ?, ?)") .bind(body.username, body.password, body.exp_date || "Never", parseInt(body.max_connections) || 1).run(); return Response.json({ success: true }); } if (pathname === "/api/users/edit") { await db.prepare("UPDATE users SET password = ?, status = ?, exp_date = ?, max_connections = ? WHERE id = ?") .bind(body.password, body.status, body.exp_date, parseInt(body.max_connections) || 1, body.id).run(); return Response.json({ success: true }); } if (pathname === "/api/users/delete") { await db.prepare("DELETE FROM users WHERE id = ?").bind(body.id).run(); return Response.json({ success: true }); } if (pathname === "/api/streams/add") { const catImg = splitCategoryImage(body.category || "Live"); const imageUrl = body.image_url || catImg.image_url || ""; await db.prepare("INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type) VALUES (?, ?, ?, ?, ?, ?, ?)") .bind(body.name, body.url, catImg.category || "Live", imageUrl, body.user_agent || "", body.referer || "", "live").run(); return Response.json({ success: true }); } if (pathname === "/api/vod/add") { const vod = await buildVodPayload(body); await db.prepare("INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type, tmdb_id, tmdb_type, tmdb_poster_url, tmdb_backdrop_url, tmdb_overview, tmdb_year, tmdb_rating) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") .bind(vod.name, vod.url, vod.category, vod.image_url || vod.tmdb_poster_url || "", vod.user_agent, vod.referer, "vod", vod.tmdb_id, vod.tmdb_type, vod.tmdb_poster_url, vod.tmdb_backdrop_url, vod.tmdb_overview, vod.tmdb_year, vod.tmdb_rating).run(); return Response.json({ success: true, vod }); } if (pathname === "/api/backup/sql_import") { const sql = body.sql; if (!sql || typeof sql !== "string") { return Response.json({ error: "Invalid SQL input" }, { status: 400 }); } const cleaned = sql .replace(/BEGIN TRANSACTION;?/gi, "") .replace(/COMMIT;?/gi, ""); const statements = cleaned .split(";\n") .map(s => s.trim()) .filter(Boolean); const errors = []; let successCount = 0; for (const stmt of statements) { try { await db.prepare(stmt).run(); successCount++; } catch (e) { errors.push({ statement: stmt.slice(0, 120), error: e.message }); } } return Response.json({ success: true, executed: successCount, failed: errors.length, errors }); } if (pathname === "/api/backup/sql") { const users = await db.prepare("SELECT * FROM users").all(); const streams = await db.prepare("SELECT * FROM streams").all(); const proxies = await db.prepare("SELECT * FROM proxies").all(); const esc = (v) => String(v ?? "").replace(/'/g, "''"); let sql = "-- TFMS IPTV Backup\nBEGIN TRANSACTION;\n\n"; for (const u of users.results) { sql += `INSERT INTO users (id, username, password, status, exp_date, max_connections) VALUES (` + `${u.id}, '${esc(u.username)}', '${esc(u.password)}', '${esc(u.status)}', '${esc(u.exp_date)}', ${u.max_connections || 1});\n`; } sql += "\n"; for (const s of streams.results) { sql += `INSERT INTO streams (id, name, url, category, image_url, user_agent, referer, content_type, tmdb_id, tmdb_type, tmdb_poster_url, tmdb_backdrop_url, tmdb_overview, tmdb_year, tmdb_rating) VALUES (` + `${s.id}, '${esc(s.name)}', '${esc(s.url)}', '${esc(s.category)}', '${esc(s.image_url)}', '${esc(s.user_agent)}', '${esc(s.referer)}', '${esc(s.content_type || 'live')}', '${esc(s.tmdb_id)}', '${esc(s.tmdb_type)}', '${esc(s.tmdb_poster_url)}', '${esc(s.tmdb_backdrop_url)}', '${esc(s.tmdb_overview)}', '${esc(s.tmdb_year)}', '${esc(s.tmdb_rating)}');\n`; } sql += "\n"; for (const p of proxies.results) { sql += `INSERT INTO proxies (id, name, url) VALUES (` + `${p.id}, '${esc(p.name)}', '${esc(p.url)}');\n`; } sql += "\nCOMMIT;"; return new Response(sql, { headers: { "Content-Type": "text/plain", "Content-Disposition": `attachment; filename="tfms_backup.sql"`, "Access-Control-Allow-Origin": "*" } }); } if (pathname === "/api/streams/mass_import") { const lines = body.m3u.split("\n"); const forcedCategory = body.category?.trim(); let currentname = "unknown stream"; let currentcategory = forcedCategory || "imported"; let currentimage = ""; for (let line of lines) { line = line.trim(); if (line.toLowerCase().startsWith("#extinf:")) { const attrs = parseM3uAttributes(line); const namematch = line.match(/,(.*)$/); if (namematch) currentname = namematch[1].trim(); currentcategory = forcedCategory || attrs["group-title"] || "imported"; currentimage = attrs["tvg-logo"] || attrs["logo"] || ""; const catImg = splitCategoryImage(currentcategory); currentcategory = catImg.category || "imported"; currentimage = currentimage || catImg.image_url || ""; } else if (line.toLowerCase().startsWith("http")) { await db.prepare("INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type) VALUES (?, ?, ?, ?, ?, ?, ?)") .bind(currentname, line, currentcategory, currentimage, "", "", "live").run(); currentname = "unknown stream"; currentcategory = forcedCategory || "imported"; currentimage = ""; } } return Response.json({ success: true }); } if (pathname === "/api/streams/import_url") { const url = body.url; if (!url || typeof url !== "string") { return Response.json({ error: "Missing URL" }, { status: 400 }); } let m3uText; try { const res = await fetch(url); if (!res.ok) { return Response.json({ error: "Failed to fetch playlist" }, { status: 500 }); } m3uText = await res.text(); } catch (e) { return Response.json({ error: e.message }, { status: 500 }); } const lines = m3uText.split("\n"); const forcedCategory = body.category?.trim(); let currentname = "unknown stream"; let currentcategory = forcedCategory || "imported"; let currentimage = ""; for (let line of lines) { line = line.trim(); if (line.toLowerCase().startsWith("#extinf:")) { const attrs = parseM3uAttributes(line); const namematch = line.match(/,(.*)$/); if (namematch) currentname = namematch[1].trim(); currentcategory = forcedCategory || attrs["group-title"] || "imported"; currentimage = attrs["tvg-logo"] || attrs["logo"] || ""; const catImg = splitCategoryImage(currentcategory); currentcategory = catImg.category || "imported"; currentimage = currentimage || catImg.image_url || ""; } else if (line.toLowerCase().startsWith("http")) { await db.prepare( "INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type) VALUES (?, ?, ?, ?, ?, ?, ?)" ).bind(currentname, line, currentcategory, currentimage, "", "", "live").run(); currentname = "unknown stream"; currentcategory = forcedCategory || "imported"; currentimage = ""; } } return Response.json({ success: true }); } if (pathname === "/api/vod/mass_import") { const lines = body.m3u.split("\n"); const forcedCategory = body.category?.trim(); let currentname = "unknown vod"; let currentcategory = forcedCategory || "VOD"; let currentimage = ""; for (let line of lines) { line = line.trim(); if (line.toLowerCase().startsWith("#extinf:")) { const attrs = parseM3uAttributes(line); const namematch = line.match(/,(.*)$/); if (namematch) currentname = namematch[1].trim(); currentcategory = forcedCategory || attrs["group-title"] || "VOD"; currentimage = attrs["tvg-logo"] || attrs["logo"] || ""; const catImg = splitCategoryImage(currentcategory); currentcategory = catImg.category || "VOD"; currentimage = currentimage || catImg.image_url || ""; } else if (line.toLowerCase().startsWith("http")) { await db.prepare("INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type) VALUES (?, ?, ?, ?, ?, ?, ?)") .bind(currentname, line, currentcategory, currentimage, "", "", "vod").run(); currentname = "unknown vod"; currentcategory = forcedCategory || "VOD"; currentimage = ""; } } return Response.json({ success: true }); } if (pathname === "/api/vod/import_url") { const url = body.url; if (!url || typeof url !== "string") { return Response.json({ error: "Missing URL" }, { status: 400 }); } let m3uText; try { const res = await fetch(url); if (!res.ok) { return Response.json({ error: "Failed to fetch VOD playlist" }, { status: 500 }); } m3uText = await res.text(); } catch (e) { return Response.json({ error: e.message }, { status: 500 }); } const lines = m3uText.split("\n"); const forcedCategory = body.category?.trim(); let currentname = "unknown vod"; let currentcategory = forcedCategory || "VOD"; let currentimage = ""; for (let line of lines) { line = line.trim(); if (line.toLowerCase().startsWith("#extinf:")) { const attrs = parseM3uAttributes(line); const namematch = line.match(/,(.*)$/); if (namematch) currentname = namematch[1].trim(); currentcategory = forcedCategory || attrs["group-title"] || "VOD"; currentimage = attrs["tvg-logo"] || attrs["logo"] || ""; const catImg = splitCategoryImage(currentcategory); currentcategory = catImg.category || "VOD"; currentimage = currentimage || catImg.image_url || ""; } else if (line.toLowerCase().startsWith("http")) { await db.prepare("INSERT INTO streams (name, url, category, image_url, user_agent, referer, content_type) VALUES (?, ?, ?, ?, ?, ?, ?)") .bind(currentname, line, currentcategory, currentimage, "", "", "vod").run(); currentname = "unknown vod"; currentcategory = forcedCategory || "VOD"; currentimage = ""; } } return Response.json({ success: true }); } if (pathname === "/api/streams/edit") { const catImg = splitCategoryImage(body.category || "Live"); const imageUrl = body.image_url || catImg.image_url || ""; await db.prepare("UPDATE streams SET name = ?, url = ?, category = ?, image_url = ?, user_agent = ?, referer = ? WHERE id = ?") .bind(body.name, body.url, catImg.category || "Live", imageUrl, body.user_agent || "", body.referer || "", body.id).run(); return Response.json({ success: true }); } if (pathname === "/api/streams/delete") { await db.prepare("DELETE FROM streams WHERE id = ?").bind(body.id).run(); return Response.json({ success: true }); } if (pathname === "/api/vod/edit") { const vod = await buildVodPayload(body); await db.prepare("UPDATE streams SET name = ?, url = ?, category = ?, image_url = ?, user_agent = ?, referer = ?, content_type = 'vod', tmdb_id = ?, tmdb_type = ?, tmdb_poster_url = ?, tmdb_backdrop_url = ?, tmdb_overview = ?, tmdb_year = ?, tmdb_rating = ? WHERE id = ?") .bind(vod.name, vod.url, vod.category, vod.image_url || vod.tmdb_poster_url || "", vod.user_agent, vod.referer, vod.tmdb_id, vod.tmdb_type, vod.tmdb_poster_url, vod.tmdb_backdrop_url, vod.tmdb_overview, vod.tmdb_year, vod.tmdb_rating, body.id).run(); return Response.json({ success: true, vod }); } if (pathname === "/api/vod/delete") { await db.prepare("DELETE FROM streams WHERE id = ? AND content_type = 'vod'").bind(body.id).run(); return Response.json({ success: true }); } if (pathname === "/api/streams/mass_delete") { if (body.scope === "all") { await db.prepare("DELETE FROM streams").run(); } else if (body.scope === "category" && body.category) { await db.prepare("DELETE FROM streams WHERE category = ?").bind(body.category).run(); } return Response.json({ success: true }); } if (pathname === "/api/proxies/add") { await db.prepare("INSERT INTO proxies (name, url) VALUES (?, ?)") .bind(body.name, body.url).run(); return Response.json({ success: true }); } if (pathname === "/api/proxies/delete") { await db.prepare("DELETE FROM proxies WHERE id = ?").bind(body.id).run(); return Response.json({ success: true }); } } if (pathname === "/api/data") { const users = await db.prepare("SELECT * FROM users").all(); const streams = await db.prepare("SELECT * FROM streams").all(); const proxies = await db.prepare("SELECT * FROM proxies").all(); const commentRow = await db.prepare("SELECT content FROM comments WHERE id = 1").first(); const comment = commentRow?.content || ""; const mappedUsers = await Promise.all(users.results.map(async (u) => { let activeNow = 0; if (kv) { const count = await kv.get(`active_conn:${u.username}`); activeNow = count ? parseInt(count) : 0; } const expired = isAccountExpired(u.exp_date); return { ...u, active_connections: activeNow, is_expired: expired }; })); const settingsData = await db .prepare("SELECT admin_user, admin_pass, tmdb_api_key, dashboard_links FROM settings WHERE id = 1") .first(); const liveStreams = streams.results.filter(s => (s.content_type || "live") !== "vod"); const vodStreams = streams.results.filter(s => (s.content_type || "live") === "vod"); return Response.json({ users: mappedUsers, streams: liveStreams, vod: vodStreams, all_streams: streams.results, proxies: proxies.results, comment, settings: settingsData }); } const html = ` TFMS IPTV Panel
TFMS IPTV Panel

Add New Proxy Server


Proxy Configuration

Proxy Information

Supported Formats
https://domain.com/
https://domain.com/proxy?url=
https://domain.com/fetch/
https://domain.com/play?u={user}&p={pass}&url=

Tips
โ€ข Add your proxy here then select it from the dropdown in user lines
โ€ข Always include trailing slash if required
โ€ข Cloudflare Workers work best for M3U routing
โ€ข You can use placeholders like {user} and {pass}

Configured Proxy Servers


Saved Proxy Routes

Proxy Tools & Resources


Proxy Creation Tools
Proxy Resources

Install this 1 Click Cloudflare Deploy

When created enter the proxy url into the box below (with tokens & user/pass)

Install this 1 Click Cloudflare Deploy

When created enter the proxy url into the box below (with tokens & NO pass)

Custom Headless Browser One Click Huggingface Deploy

Click Duplicate Space then paste your proxy url into the box below then click load site
Retrieve streams & referers from embed pages
๐Ÿ“ข Announcement:
Note this panel plays the direct links behind proxies, If you are using a 1 connection playlist as your stream source this will not work when you are serving multiple users, Always use streams from a good multi connection playlist

Get FREE Streams


Live Media Players


Tools


Admin Settings


Change Admin Login

Admin Information

Security Notice
Changing admin credentials will immediately affect login access.
Session System
Existing login cookies may require browser refresh after updates.
Best Practice
Use strong passwords and avoid default credentials.

TMDB API Settings


TMDB Key

TMDB Information

VOD Auto-Fill
This key is used to search TMDB and auto-fill posters, backdrop images, overview, year, and rating for VOD entries.
Priority
If a Cloudflare environment variable named TMDB_API_KEY exists, it will be used before the saved database key.
Privacy
Keep your TMDB key private and only save keys you control.

D1 SQL Backup Tools


Import/Export SQL Backup

Paste a full SQL backup export below and click import.

Here You Can Import/Export Your SQL Backup


Export a full SQL backup containing:
โ€ข Users, Streams, Proxies, Settings

Import a full SQL backup:
Open Your SQL Backup On Your PC & Copy The Contents & Paste Into The Box

If This Fails Use Cloudflare Dashboard
Total Users
0
Registered Lines
Total Streams & VOD
0
Active Channels
Proxies
0
Routing Nodes
System Status
ONLINE
All services running
Sticky System Notes
TFMS IPTV Panel v1.0.5
What's New in This Release
  • New VOD section with TMDB
  • Categories & Image fields updated
  • Built in proxy encodes the urls
  • 2 new proxy options with tokens
  • Choose proxy per userline
  • Copy playlist button
  • Tinyurl button auto generated
  • Headless browser for finding streams
  • User-agent & referal options
  • Edit Quick Links
Coming Next
โ€ข Big code clean up
โ€ข Anything else i can think of
Dashboard Quick Links

User Line Registry


Create & Edit Account

Line Information


Username & Password
Unique account login used for playlist generation
Max Connections
Controls simultaneous active streams allowed per account
Expiration Date
Accounts automatically stop working after 23:59 on selected date
Status Types
Active = User can stream normally
Disabled = Account blocked manually
Playlist Downloads
Generate playlists using direct streams, built-in proxy, or custom proxies
Hardcoded EPG
TV-Guide is hardcoded your iptv app should pick it up
NOTES
Only using the built in proxy will hide the real stream url this is to avoid double proxying and helps avoid cloudflares TOS

Registered User Lines


Subscriber Conns (Live/Max) Status
Actions

VOD Library Management


Create & Edit VOD Files

VOD Bulk Import


OR Import VOD From URL

Registered VOD Files


VOD Name Category
Actions

Streams & VOD Management


Create & Edit Streams & VOD



Optional User-Agent and Referer are sent by the built-in proxy and added as VLC options in generated playlists.

M3U Bulk Import (.m3u parsing)


OR Import From URL

Registered Streams & VOD


Channel Name Group Tag
Actions

Mass Delete Tools


Mass Delete:
`; return new Response(html, { headers: { "Content-Type": "text/html" } }); } };