mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-11 19:05:41 -04:00
initial push from external dev branches
This commit is contained in:
BIN
Screenshots/Styles-Colors.png
Normal file
BIN
Screenshots/Styles-Colors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/TVApp2-Logo.png
Normal file
BIN
Screenshots/TVApp2-Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
5
config/config.example.json
Normal file
5
config/config.example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mongoUri": "mongodb://MONGO_ROOT_USER:MONGO_ROOT_PASS@mongo:27017/tvapp2?authSource=admin",
|
||||
"port": 3000,
|
||||
"logLevel": "info"
|
||||
}
|
||||
5
config/config.json
Normal file
5
config/config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mongoUri": "mongodb://tvapp:tvapp@mongo:27017/tvapp2?authSource=admin",
|
||||
"port": 3000,
|
||||
"logLevel": "info"
|
||||
}
|
||||
1
dist/assets/ActiveStreamsScreen-Df8Wdk9n.js
vendored
Normal file
1
dist/assets/ActiveStreamsScreen-Df8Wdk9n.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/DashboardScreen-C8mAn1wl.js
vendored
Normal file
1
dist/assets/DashboardScreen-C8mAn1wl.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/EPGDetailScreen-CX4y1Ve9.js
vendored
Normal file
1
dist/assets/EPGDetailScreen-CX4y1Ve9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/EPGSourcesScreen-Bh2qXoOm.js
vendored
Normal file
1
dist/assets/EPGSourcesScreen-Bh2qXoOm.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{v as g,r,o as e,u as n,X as o,e as d,F as y,K as p,O as u,d as b,H as l,t as a,y as h,f as c,N as i,g as k,p as C,_ as m,q as S,Q as $,j as x}from"./index-CQPQcDLN.js";const E={class:"col"},z={class:"card flush"},N={class:"toolbar"},w=["onClick"],B={class:"src-name"},G={class:"src-url"},P={class:"stat-mini"},V={class:"stat-mini"},F={class:"stat-mini",style:{"min-width":"110px"}},L={style:{"font-size":"12px","font-weight":"500",color:"var(--text-1)"}},A=g({__name:"EPGSourcesScreen",emits:["add"],setup(O,{emit:_}){const v=_,f=$();return(R,s)=>(l(),r("div",E,[e("div",z,[e("div",N,[n(x,{value:"",onChange:()=>{},placeholder:"Search EPG sources"}),s[3]||(s[3]=e("span",{class:"spacer"},null,-1)),n(d,{variant:"ghost",icon:"refresh"},{default:o(()=>[...s[1]||(s[1]=[a("Sync all",-1)])]),_:1}),n(d,{variant:"primary",icon:"plus",onClick:s[0]||(s[0]=t=>v("add","epg"))},{default:o(()=>[...s[2]||(s[2]=[a("Add EPG source",-1)])]),_:1})]),(l(!0),r(y,null,p(u(b),t=>(l(),r("div",{key:t.id,class:"src-row",onClick:j=>u(f).push(`/epg-sources/${t.id}`)},[e("div",{class:h(["src-ico",{builtin:t.builtin,"epg-builtin":t.builtin}]),style:{color:"var(--good)"}},[n(c,{name:t.builtin?"tv":"epg",size:18},null,8,["name"])],2),e("div",null,[e("div",B,[a(i(t.name)+" ",1),n(k,{status:t.status,pulse:t.status==="good"},null,8,["status","pulse"]),t.builtin?(l(),C(m,{key:0,tone:"system"},{default:o(()=>[n(c,{name:"check",size:10}),s[4]||(s[4]=a("built-in",-1))]),_:1})):S("",!0),n(m,{tone:"cyan"},{default:o(()=>[a(i(t.interval),1)]),_:2},1024)]),e("div",G,i(t.url),1)]),e("div",P,[e("b",null,i(t.channels),1),s[5]||(s[5]=a("channels",-1))]),e("div",V,[e("b",null,i(t.programs.toLocaleString()),1),s[6]||(s[6]=a("programs",-1))]),e("div",F,[e("b",L,i(t.lastSync),1),s[7]||(s[7]=a(" last sync ",-1))]),n(d,{variant:"ghost",size:"sm",icon:"more"})],8,w))),128))])]))}});export{A as default};
|
||||
1
dist/assets/HistoryMetricsScreen-DAcDrLQC.js
vendored
Normal file
1
dist/assets/HistoryMetricsScreen-DAcDrLQC.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/ImportScreen-D7vLRk6-.js
vendored
Normal file
1
dist/assets/ImportScreen-D7vLRk6-.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/MappingScreen-BdiMBcth.js
vendored
Normal file
1
dist/assets/MappingScreen-BdiMBcth.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{v as V,C as f,r,o as n,u as i,O as z,z as A,X as m,e as w,t as u,y as c,F as g,K as N,q as $,_ as k,I as D,J as B,n as E,H as l,N as p,p as y,f as v,i as G}from"./index-CQPQcDLN.js";import{_ as U}from"./Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js";const K={class:"col"},T={class:"card",style:{display:"flex","align-items":"center",gap:"18px"}},j={style:{width:"180px",height:"6px",background:"var(--bg-2)","border-radius":"999px",overflow:"hidden"}},F={class:"map-grid"},O={class:"map-col"},P={class:"segmented",style:{padding:"2px"}},H={class:"map-list"},J=["onClick"],L={class:"nm"},q={class:"id"},X={key:0,class:"empty"},Q={class:"map-link",style:{"align-self":"center"}},R={class:"map-col"},W={class:"map-list"},Y=["onClick"],Z={class:"nm"},ee={class:"id"},ae=V({__name:"MappingScreen",setup(ne){const _=[{id:"bbc.one.uk",name:"BBC One"},{id:"bbc.two.uk",name:"BBC Two"},{id:"bbc.news.uk",name:"BBC News"},{id:"sky.sports.main.uk",name:"Sky Sports Main Event"},{id:"sky.sports.f1.uk",name:"Sky Sports F1"},{id:"itv1.uk",name:"ITV1"},{id:"channel4.uk",name:"Channel 4"},{id:"film4.uk",name:"Film4"},{id:"discovery.uk",name:"Discovery Channel UK"},{id:"natgeo.uk",name:"National Geographic UK"},{id:"cnn.int",name:"CNN International"},{id:"aljazeera.en",name:"Al Jazeera English"},{id:"hgtv.uk",name:"HGTV UK"},{id:"nickjr.uk",name:"Nick Jr UK"},{id:"tcm.uk",name:"TCM Movies"},{id:"eurosport1.uk",name:"Eurosport 1"}],a=D({});f.forEach(s=>{s.epg==="matched"&&s.tvg_id&&(a[s.id]=s.tvg_id)});const o=B(null),d=B("unmatched"),x=E(()=>f.filter(s=>d.value==="all"||(d.value==="unmatched"?!a[s.id]:!!a[s.id])));function S(s,e){a[s]=e}function I(s){delete a[s]}const C=E(()=>Object.keys(a).length),b=f.length;function h(s){return Object.entries(a).find(([,e])=>e===s)}return(s,e)=>(l(),r("div",K,[n("div",T,[i(v,{name:"map",size:20}),e[4]||(e[4]=n("div",{style:{flex:"1"}},[n("div",{style:{"font-weight":"600","font-size":"15px"}},"Channel ↔ EPG mapping"),n("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}}," Drag from left to right, or pick a channel and click the EPG ID. Auto-match runs nightly. ")],-1)),i(U,{label:"Matched",value:`${C.value} / ${z(b)}`},null,8,["value"]),n("div",j,[n("div",{style:A({width:C.value/z(b)*100+"%",height:"100%",background:"var(--accent)",boxShadow:"0 0 12px var(--accent)"})},null,4)]),i(w,{variant:"primary",icon:"refresh"},{default:m(()=>[...e[3]||(e[3]=[u("Auto-match",-1)])]),_:1})]),n("div",F,[n("div",O,[n("h3",null,[i(v,{name:"playlist",size:14}),e[5]||(e[5]=u(" M3U Channels ",-1)),e[6]||(e[6]=n("span",{class:"spacer"},null,-1)),n("div",P,[n("button",{class:c(d.value==="unmatched"?"active":""),onClick:e[0]||(e[0]=t=>d.value="unmatched"),style:{"font-size":"10.5px",padding:"3px 8px"}},"Unmatched",2),n("button",{class:c(d.value==="matched"?"active":""),onClick:e[1]||(e[1]=t=>d.value="matched"),style:{"font-size":"10.5px",padding:"3px 8px"}},"Matched",2),n("button",{class:c(d.value==="all"?"active":""),onClick:e[2]||(e[2]=t=>d.value="all"),style:{"font-size":"10.5px",padding:"3px 8px"}},"All",2)])]),n("div",H,[(l(!0),r(g,null,N(x.value,t=>(l(),r("div",{key:t.id,class:c(["map-item",{selected:o.value===t.id,matched:!!a[t.id]}]),onClick:M=>o.value=t.id},[i(G,{ch:t},null,8,["ch"]),n("div",L,p(t.tvg_name),1),a[t.id]?(l(),r(g,{key:0},[n("span",q,p(a[t.id]),1),i(w,{variant:"ghost",size:"sm",icon:"x",onClick:M=>I(t.id)},null,8,["onClick"])],64)):(l(),y(k,{key:1,tone:"warn"},{default:m(()=>[...e[7]||(e[7]=[u("unmatched",-1)])]),_:1}))],10,J))),128)),x.value.length===0?(l(),r("div",X,[...e[8]||(e[8]=[n("h3",null,"All matched 🎉",-1),n("p",null,"Every channel in this view has an EPG ID assigned.",-1)])])):$("",!0)])]),n("div",Q,[i(v,{name:"chevron-r",size:22})]),n("div",R,[n("h3",null,[i(v,{name:"epg",size:14}),e[9]||(e[9]=u(" EPG channel IDs ",-1)),e[10]||(e[10]=n("span",{class:"spacer"},null,-1)),i(k,null,{default:m(()=>[u(p(_.length),1)]),_:1})]),n("div",W,[(l(),r(g,null,N(_,t=>n("div",{key:t.id,class:c(["map-item",{selected:o.value&&!h(t.id),matched:!!h(t.id)}]),onClick:()=>{o.value&&!h(t.id)&&(S(o.value,t.id),o.value=null)}},[n("div",Z,p(t.name),1),n("span",ee,p(t.id),1),h(t.id)?(l(),y(k,{key:0,tone:"good"},{default:m(()=>[i(v,{name:"check",size:10}),e[11]||(e[11]=u("linked",-1))]),_:1})):o.value?(l(),y(k,{key:1,tone:"cyan"},{default:m(()=>[...e[12]||(e[12]=[u("click to link",-1)])]),_:1})):$("",!0)],10,Y)),64))])])])]))}});export{ae as default};
|
||||
1
dist/assets/PlaylistDetailScreen-F92VSAQ7.js
vendored
Normal file
1
dist/assets/PlaylistDetailScreen-F92VSAQ7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/PlaylistsScreen-0ooKY6SX.js
vendored
Normal file
1
dist/assets/PlaylistsScreen-0ooKY6SX.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{v as k,D as g,r as d,o as a,u as i,X as l,e as _,F as h,K as C,J as $,H as o,t as e,O as u,y as x,f as v,N as n,g as w,p as S,_ as c,q as N,Q as z,j as B}from"./index-CQPQcDLN.js";import{u as f}from"./useSettings-CPUgOpin.js";const V={class:"col"},j={class:"card flush"},A={class:"toolbar"},D=["onClick"],F={class:"src-name"},P={class:"src-url"},q={class:"stat-mini"},E={class:"stat-mini"},H={class:"stat-mini",style:{"min-width":"110px"}},I={style:{"font-size":"12px","font-weight":"500",color:"var(--text-1)"}},O=k({__name:"PlaylistsScreen",emits:["add"],setup(J,{emit:y}){const p=y,b=z(),m=$([]);return g(async()=>{const r=await fetch("/api/playlists");r.ok&&(m.value=await r.json())}),(r,s)=>(o(),d("div",V,[a("div",j,[a("div",A,[i(B,{value:"",onChange:()=>{},placeholder:"Search playlists"}),s[3]||(s[3]=a("span",{class:"spacer"},null,-1)),i(_,{variant:"ghost",icon:"refresh"},{default:l(()=>[...s[1]||(s[1]=[e("Sync all",-1)])]),_:1}),i(_,{variant:"primary",icon:"plus",onClick:s[0]||(s[0]=t=>p("add","playlist"))},{default:l(()=>[...s[2]||(s[2]=[e("Add playlist",-1)])]),_:1})]),(o(!0),d(h,null,C(m.value,t=>(o(),d("div",{key:t.id,class:"src-row",onClick:K=>u(b).push(`/playlists/${t.id}`)},[a("div",{class:x(["src-ico",{builtin:t.builtin}])},[i(v,{name:t.builtin?"tv":"playlist",size:18},null,8,["name"])],2),a("div",null,[a("div",F,[e(n(t.name)+" ",1),i(w,{status:t.status,pulse:t.status==="good"},null,8,["status","pulse"]),t.builtin?(o(),S(c,{key:0,tone:"system"},{default:l(()=>[i(v,{name:"check",size:10}),s[4]||(s[4]=e("built-in",-1))]),_:1})):N("",!0),i(c,{tone:"cyan"},{default:l(()=>[e(n(t.interval),1)]),_:2},1024)]),a("div",P,n(t.url),1)]),i(c,{tone:u(f)(t.id).active?"active":"disabled"},{default:l(()=>[e(n(u(f)(t.id).active?"Active":"Inactive"),1)]),_:2},1032,["tone"]),a("div",q,[a("b",null,n(t.channels),1),s[5]||(s[5]=e("channels",-1))]),a("div",E,[a("b",null,n(t.groups),1),s[6]||(s[6]=e("groups",-1))]),a("div",H,[a("b",I,n(t.lastSync),1),s[7]||(s[7]=e(" last sync ",-1))])],8,D))),128))])]))}});export{O as default};
|
||||
1
dist/assets/SettingsScreen-Daj_a2gr.js
vendored
Normal file
1
dist/assets/SettingsScreen-Daj_a2gr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js
vendored
Normal file
1
dist/assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{v as l,r as o,o as t,N as a,z as n,L as r,H as i,t as m}from"./index-CQPQcDLN.js";const c={style:{"text-align":"right"}},d={class:"muted",style:{"font-size":"var(--fs-xs)","text-transform":"uppercase","letter-spacing":"0.06em"}},u=l({__name:"Stat",props:{label:{},value:{},small:{type:Boolean}},setup(e){return(s,f)=>(i(),o("div",c,[t("div",d,a(e.label),1),t("div",{style:n({fontWeight:600,fontSize:e.small?"var(--fs-sm)":"18px",fontVariantNumeric:"tabular-nums",marginTop:"2px"})},[r(s.$slots,"default",{},()=>[m(a(e.value),1)])],4)]))}});export{u as _};
|
||||
1
dist/assets/index-BzPI8e-F.css
vendored
Normal file
1
dist/assets/index-BzPI8e-F.css
vendored
Normal file
File diff suppressed because one or more lines are too long
26
dist/assets/index-CQPQcDLN.js
vendored
Normal file
26
dist/assets/index-CQPQcDLN.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/useSettings-CPUgOpin.js
vendored
Normal file
1
dist/assets/useSettings-CPUgOpin.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{n as c,J as s,I as l}from"./index-CQPQcDLN.js";const h=s("TVApp2 Workspace"),u=s("https://tvapp2.example.com"),o=s("/m3u/playlist.m3u8"),$=s("/epg/guide.xml.gz"),m=c(()=>`${u.value.replace(/\/$/,"")}${o.value.startsWith("/")?"":"/"}${o.value}`),e=l({});function i(t){return e[t]||(e[t]={active:!0,endpointMode:"global",customPath:`/playlists/${t}.m3u`}),e[t]}function d(t){const a=i(t),n=u.value.replace(/\/$/,"");if(a.endpointMode==="custom"){const p=a.customPath.startsWith("/")?a.customPath:`/${a.customPath}`;return`${n}${p}`}return m.value}export{u as a,o as b,h as d,$ as e,m,d as p,i as u};
|
||||
19
dist/index.html
vendored
Normal file
19
dist/index.html
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark" data-density="regular">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TVApp2 — M3U & EPG Manager</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
@keyframes slidein { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-CQPQcDLN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BzPI8e-F.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
2
dist/types-node/vite.config.d.ts
vendored
Normal file
2
dist/types-node/vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
export default _default;
|
||||
14
dist/types-node/vite.config.js
vendored
Normal file
14
dist/types-node/vite.config.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
84
docker-compose.yml
Normal file
84
docker-compose.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# Pinned image versions — bump in lockstep with docker/*.Dockerfile.
|
||||
# MONGO = 7.0.15
|
||||
# Requires Docker Engine >= 24.0 and Docker Compose >= 2.20 (Compose Spec).
|
||||
# The legacy top-level `version:` key is intentionally omitted (deprecated in Compose v2).
|
||||
|
||||
services:
|
||||
# One-shot init: writes ${TVAPP2_CONFIG_DIR}/config.json from the MONGO_ROOT_USER /
|
||||
# MONGO_ROOT_PASS values in .env. The JSON template is embedded below — NO host file is
|
||||
# bind-mounted — so a deployment host needs only this compose file + .env, nothing else to
|
||||
# ship. (Bind-mounting a host template is a footgun: if the file is missing Docker silently
|
||||
# creates it as an empty *directory*, and config.json ends up empty.) Left untouched if an
|
||||
# existing config.json already targets the compose "mongo" host; regenerated otherwise
|
||||
# (e.g. a stale @127.0.0.1: config left by an all-in-one run).
|
||||
config-init:
|
||||
image: alpine:3.20
|
||||
environment:
|
||||
MONGO_ROOT_USER: ${MONGO_ROOT_USER}
|
||||
MONGO_ROOT_PASS: ${MONGO_ROOT_PASS}
|
||||
volumes:
|
||||
- ${TVAPP2_CONFIG_DIR}:/out
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
if [ -s /out/config.json ] && grep -q '@mongo:' /out/config.json; then
|
||||
echo "[config-init] /out/config.json already targets @mongo: — leaving alone"
|
||||
exit 0
|
||||
fi
|
||||
if [ -s /out/config.json ]; then
|
||||
echo "[config-init] /out/config.json does not target @mongo: — regenerating"
|
||||
fi
|
||||
printf '{\n "mongoUri": "mongodb://%s:%s@mongo:27017/tvapp2?authSource=admin",\n "port": 3000,\n "logLevel": "info"\n}\n' \
|
||||
"$$MONGO_ROOT_USER" "$$MONGO_ROOT_PASS" > /out/config.json
|
||||
echo "[config-init] wrote /out/config.json"
|
||||
restart: "no"
|
||||
|
||||
app:
|
||||
image: iflip721/tvapp2-app-stack:2.0.5-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/app.Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
# config.json is generated by the config-init service from .env (embedded template).
|
||||
# Set TVAPP2_CONFIG_DIR in .env to the host directory that holds it.
|
||||
- ${TVAPP2_CONFIG_DIR}:/etc/tvapp2:rw
|
||||
depends_on:
|
||||
config-init:
|
||||
condition: service_completed_successfully
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
networks: [tvnet]
|
||||
restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
image: mongo:7.0.15
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASS}
|
||||
ports:
|
||||
# Expose mongod on the host. Override the host-side port with
|
||||
# MONGO_HOST_PORT in .env (defaults to 27017).
|
||||
- "${MONGO_HOST_PORT:-27017}:27017"
|
||||
volumes:
|
||||
# Persistent MongoDB data. Set MONGO_DATA_PATH in .env to an absolute
|
||||
# host path (e.g. /srv/tvapp2/mongo) for a bind mount; leave unset to
|
||||
# use the named volume defined below.
|
||||
- ${MONGO_DATA_PATH:-mongo-data}:/data/db
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks: [tvnet]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
# Fallback named volume — used only when MONGO_DATA_PATH is unset.
|
||||
mongo-data:
|
||||
|
||||
networks:
|
||||
tvnet:
|
||||
65
docker/app.Dockerfile
Normal file
65
docker/app.Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# -----------------------------------------------------------------------------
|
||||
# docker/app.Dockerfile — TVApp2 "app stack" image (iflip721/tvapp2-app-stack)
|
||||
#
|
||||
# Three-stage build:
|
||||
# 1) spa-build — Vue 3 + Vite SPA → /spa/dist (root package.json)
|
||||
# 2) server-build — Express API (tsc) → /server/dist (server/package.json)
|
||||
# 3) runtime — prod-only Node; serves API + built SPA on :3000
|
||||
#
|
||||
# Runtime layout (must match server/src/index.ts publicDir and sources/paths.ts SEED_SOURCES_DIR):
|
||||
# /app/dist/ compiled server (dist/index.js, dist/sources/paths.js)
|
||||
# /app/public/ built SPA (resolve(<dist>,'..','public') => /app/public)
|
||||
# /app/seed-data/ source bundles (resolve(<dist>,'..','..','seed-data','sources'))
|
||||
# /app/package.json server pkg (type:module) + node_modules (express, mongoose only)
|
||||
#
|
||||
# Node pin: 22.11.0 LTS "Jod" on alpine 3.20 — keep in lockstep with CLAUDE.md.
|
||||
# -----------------------------------------------------------------------------
|
||||
ARG NODE_IMAGE=node:22.11.0-alpine3.20
|
||||
|
||||
# ---- Stage 1: build the SPA (root package) ----------------------------------
|
||||
FROM ${NODE_IMAGE} AS spa-build
|
||||
WORKDIR /spa
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json tsconfig.node.json vite.config.ts index.html ./
|
||||
COPY public/ ./public/
|
||||
COPY src/ ./src/
|
||||
RUN npm run build # vue-tsc -b && vite build -> /spa/dist
|
||||
|
||||
# ---- Stage 2: build the server (server package) -----------------------------
|
||||
FROM ${NODE_IMAGE} AS server-build
|
||||
WORKDIR /server
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci # devDeps (typescript) needed to compile
|
||||
COPY server/tsconfig.json ./
|
||||
COPY server/src/ ./src/
|
||||
RUN npm run build # tsc -p . -> /server/dist
|
||||
|
||||
# ---- Stage 3: runtime -------------------------------------------------------
|
||||
FROM ${NODE_IMAGE} AS runtime
|
||||
ENV NODE_ENV=production \
|
||||
TVAPP2_CONFIG=/etc/tvapp2/config.json
|
||||
WORKDIR /app
|
||||
|
||||
# tini = correct PID 1 (forwards SIGTERM/SIGINT to the graceful-shutdown handler in index.ts).
|
||||
RUN apk add --no-cache tini
|
||||
|
||||
# Prod-only server deps (express, mongoose).
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Compiled server + built SPA + committed seed bundles.
|
||||
COPY --from=server-build /server/dist ./dist
|
||||
COPY --from=spa-build /spa/dist ./public
|
||||
COPY server/seed-data ./seed-data
|
||||
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
|
||||
# Liveness: HTTP server up (body also reports mongo connected/disconnected).
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["node", "dist/index.js"]
|
||||
18
index.html
Normal file
18
index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark" data-density="regular">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TVApp2 — M3U & EPG Manager</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
@keyframes slidein { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
m3u-data/export - M3U - Grid.xlsx
Normal file
BIN
m3u-data/export - M3U - Grid.xlsx
Normal file
Binary file not shown.
1575
package-lock.json
generated
Normal file
1575
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "tvapp2",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"hls.js": "^1.6.16",
|
||||
"mitt": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
5
server/config.local.json
Normal file
5
server/config.local.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mongoUri": "mongodb://tvapp:tvapp@127.0.0.1:27017/tvapp2?authSource=admin",
|
||||
"port": 3000,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
40
server/dist/config.js
vendored
Normal file
40
server/dist/config.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
const VALID_SCHEMES = /^mongodb(\+srv)?:\/\//;
|
||||
function resolveConfigPath() {
|
||||
const envPath = process.env.TVAPP2_CONFIG;
|
||||
if (envPath) {
|
||||
if (!existsSync(envPath)) {
|
||||
throw new Error(`TVAPP2_CONFIG points to "${envPath}" but the file does not exist.`);
|
||||
}
|
||||
return envPath;
|
||||
}
|
||||
const localPath = resolve(process.cwd(), 'config.local.json');
|
||||
if (existsSync(localPath))
|
||||
return localPath;
|
||||
throw new Error('No config file found. Set TVAPP2_CONFIG to the mounted config path, ' +
|
||||
'or create ./config.local.json for development.');
|
||||
}
|
||||
export function loadConfig() {
|
||||
const path = resolveConfigPath();
|
||||
const raw = readFileSync(path, 'utf8');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`Config file at "${path}" is not valid JSON: ${err.message}`);
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error(`Config file at "${path}" must be a JSON object.`);
|
||||
}
|
||||
const obj = parsed;
|
||||
const mongoUri = obj.mongoUri;
|
||||
if (typeof mongoUri !== 'string' || !VALID_SCHEMES.test(mongoUri)) {
|
||||
throw new Error(`Config "mongoUri" must be a string starting with mongodb:// or mongodb+srv:// (got ${JSON.stringify(mongoUri)}).`);
|
||||
}
|
||||
const port = typeof obj.port === 'number' ? obj.port : 3000;
|
||||
const logLevel = typeof obj.logLevel === 'string' ? obj.logLevel : 'info';
|
||||
return { mongoUri, port, logLevel };
|
||||
}
|
||||
//# sourceMappingURL=config.js.map
|
||||
1
server/dist/config.js.map
vendored
Normal file
1
server/dist/config.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC1C,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,4BAA4B,OAAO,gCAAgC,CAAC,CAAC;QACvF,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAE5C,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,gDAAgD,CACnD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,wBAAyB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,0BAA0B,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,GAAG,GAAG,MAAiC,CAAC;IAE9C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC9B,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CACb,sFAAsF,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CACnH,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IAE1E,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACtC,CAAC"}
|
||||
24
server/dist/db.js
vendored
Normal file
24
server/dist/db.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import mongoose from 'mongoose';
|
||||
export async function connect(uri) {
|
||||
mongoose.connection.on('error', (err) => {
|
||||
console.error('[mongo] connection error:', err.message);
|
||||
});
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
console.warn('[mongo] disconnected');
|
||||
});
|
||||
mongoose.connection.on('reconnected', () => {
|
||||
console.info('[mongo] reconnected');
|
||||
});
|
||||
await mongoose.connect(uri, {
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
maxPoolSize: 10,
|
||||
});
|
||||
console.info('[mongo] connected');
|
||||
}
|
||||
export async function disconnect() {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
export function isConnected() {
|
||||
return mongoose.connection.readyState === 1;
|
||||
}
|
||||
//# sourceMappingURL=db.js.map
|
||||
1
server/dist/db.js.map
vendored
Normal file
1
server/dist/db.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAW;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACtC,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;QAC1C,OAAO,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,aAAa,EAAE,GAAG,EAAE;QACzC,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1B,wBAAwB,EAAE,IAAI;QAC9B,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,QAAQ,CAAC,UAAU,CAAC,UAAU,KAAK,CAAC,CAAC;AAC9C,CAAC"}
|
||||
79
server/dist/index.js
vendored
Normal file
79
server/dist/index.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import express from 'express';
|
||||
import { loadConfig } from './config.js';
|
||||
import { connect, disconnect } from './db.js';
|
||||
import { healthRouter } from './routes/health.js';
|
||||
import { playlistsRouter } from './routes/playlists.js';
|
||||
import { epgSourcesRouter } from './routes/epgSources.js';
|
||||
import { channelsRouter } from './routes/channels.js';
|
||||
import { activeStreamsRouter } from './routes/activeStreams.js';
|
||||
import { customPlaylistsRouter } from './routes/customPlaylists.js';
|
||||
import { programsRouter } from './routes/programs.js';
|
||||
import { activityRouter } from './routes/activity.js';
|
||||
import { streamSessionsRouter } from './routes/streamSessions.js';
|
||||
import { sourcesRouter } from './routes/sources.js';
|
||||
import { bootInitSources } from './sources/seed.js';
|
||||
async function main() {
|
||||
const config = loadConfig();
|
||||
try {
|
||||
await connect(config.mongoUri);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[startup] failed to connect to mongo:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
// Ingest the established (Default) source playlists: guarantee each from its committed bundle
|
||||
// (idempotent), then kick a non-blocking live sync. Runs in both Docker variants via this single
|
||||
// boot path. A failure here must not prevent the API from serving.
|
||||
try {
|
||||
await bootInitSources();
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[startup] source init error (continuing):', err.message);
|
||||
}
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/health', healthRouter);
|
||||
app.use('/api/playlists', playlistsRouter);
|
||||
app.use('/api/epg-sources', epgSourcesRouter);
|
||||
app.use('/api/channels', channelsRouter);
|
||||
app.use('/api/active-streams', activeStreamsRouter);
|
||||
app.use('/api/custom-playlists', customPlaylistsRouter);
|
||||
app.use('/api/epg-programs', programsRouter);
|
||||
app.use('/api/activity', activityRouter);
|
||||
app.use('/api/stream-sessions', streamSessionsRouter);
|
||||
// Generic source API (manifest, stream proxy, status, sync/reset) — mounted at root since its
|
||||
// paths span /api/sources and /api/v1.
|
||||
app.use(sourcesRouter);
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const publicDir = resolve(here, '..', 'public');
|
||||
if (existsSync(publicDir)) {
|
||||
app.use(express.static(publicDir));
|
||||
app.get(/^\/(?!api\/).*/, (_req, res) => {
|
||||
res.sendFile(resolve(publicDir, 'index.html'));
|
||||
});
|
||||
console.info(`[http] serving SPA from ${publicDir}`);
|
||||
}
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error('[api] error:', err.message);
|
||||
res.status(500).json({ error: 'internal_error' });
|
||||
});
|
||||
const server = app.listen(config.port, () => {
|
||||
console.info(`[api] listening on :${config.port}`);
|
||||
});
|
||||
const shutdown = async (signal) => {
|
||||
console.info(`[shutdown] received ${signal}`);
|
||||
server.close();
|
||||
await disconnect();
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => void shutdown('SIGINT'));
|
||||
}
|
||||
main().catch((err) => {
|
||||
console.error('[startup] fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
server/dist/index.js.map
vendored
Normal file
1
server/dist/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||
25
server/dist/models/ActiveStream.js
vendored
Normal file
25
server/dist/models/ActiveStream.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const ActiveStreamSchema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
channelId: { type: String, required: true, index: true },
|
||||
status: { type: String, required: true },
|
||||
uptime: { type: String, required: true },
|
||||
uptimeMin: { type: Number, required: true },
|
||||
viewers: { type: Number, required: true },
|
||||
peakViewers: { type: Number, required: true },
|
||||
bitrate: { type: Number, required: true },
|
||||
targetBitrate: { type: Number, required: true },
|
||||
codec: { type: String, required: true },
|
||||
audio: { type: String, required: true },
|
||||
container: { type: String, required: true },
|
||||
resolution: { type: String, required: true },
|
||||
fps: { type: Number, required: true },
|
||||
sourceUrl: { type: String, required: true },
|
||||
sourceHost: { type: String, required: true },
|
||||
droppedFrames: { type: Number, required: true },
|
||||
droppedRatio: { type: Number, required: true },
|
||||
latency: { type: Number, required: true },
|
||||
bandwidth: { type: Number, required: true },
|
||||
}, { versionKey: false });
|
||||
export const ActiveStream = model('ActiveStream', ActiveStreamSchema);
|
||||
//# sourceMappingURL=ActiveStream.js.map
|
||||
1
server/dist/models/ActiveStream.js.map
vendored
Normal file
1
server/dist/models/ActiveStream.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ActiveStream.js","sourceRoot":"","sources":["../../src/models/ActiveStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,kBAAkB,GAAG,IAAI,MAAM,CACnC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC7C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC/C,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC/C,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC9C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC5C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC"}
|
||||
9
server/dist/models/Activity.js
vendored
Normal file
9
server/dist/models/Activity.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const ActivitySchema = new Schema({
|
||||
when: { type: String, required: true },
|
||||
icon: { type: String, required: true },
|
||||
html: { type: String, required: true },
|
||||
order: { type: Number, required: true, index: true },
|
||||
}, { versionKey: false });
|
||||
export const Activity = model('Activity', ActivitySchema);
|
||||
//# sourceMappingURL=Activity.js.map
|
||||
1
server/dist/models/Activity.js.map
vendored
Normal file
1
server/dist/models/Activity.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Activity.js","sourceRoot":"","sources":["../../src/models/Activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,IAAI,MAAM,CAC/B;IACE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC"}
|
||||
18
server/dist/models/Channel.js
vendored
Normal file
18
server/dist/models/Channel.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const ChannelSchema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
tvg_name: { type: String, required: true },
|
||||
group: { type: String, required: true },
|
||||
channel: { type: Number, required: true },
|
||||
tvg_id: { type: String, default: null },
|
||||
state: { type: String, enum: ['active', 'disabled'], required: true },
|
||||
epg: { type: String, enum: ['matched', 'unmatched'], required: true },
|
||||
source: { type: String, required: true },
|
||||
url: { type: String, required: true, unique: true },
|
||||
status: { type: String, required: true },
|
||||
res: { type: String, required: true },
|
||||
logoColor: { type: String, required: true },
|
||||
initials: { type: String, required: true },
|
||||
}, { versionKey: false });
|
||||
export const Channel = model('Channel', ChannelSchema);
|
||||
//# sourceMappingURL=Channel.js.map
|
||||
1
server/dist/models/Channel.js.map
vendored
Normal file
1
server/dist/models/Channel.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../src/models/Channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,aAAa,GAAG,IAAI,MAAM,CAC9B;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACvC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrE,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACnD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC3C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC"}
|
||||
10
server/dist/models/CustomPlaylist.js
vendored
Normal file
10
server/dist/models/CustomPlaylist.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const CustomPlaylistSchema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
name: { type: String, required: true },
|
||||
slug: { type: String, required: true, index: true },
|
||||
channels: { type: Number, required: true },
|
||||
updated: { type: String, required: true },
|
||||
}, { versionKey: false });
|
||||
export const CustomPlaylist = model('CustomPlaylist', CustomPlaylistSchema);
|
||||
//# sourceMappingURL=CustomPlaylist.js.map
|
||||
1
server/dist/models/CustomPlaylist.js.map
vendored
Normal file
1
server/dist/models/CustomPlaylist.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"CustomPlaylist.js","sourceRoot":"","sources":["../../src/models/CustomPlaylist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,oBAAoB,GAAG,IAAI,MAAM,CACrC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACnD,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC1C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,CAAC"}
|
||||
15
server/dist/models/EpgSource.js
vendored
Normal file
15
server/dist/models/EpgSource.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const EpgSourceSchema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
name: { type: String, required: true },
|
||||
url: { type: String, required: true },
|
||||
channels: { type: Number, required: true },
|
||||
programs: { type: Number, required: true },
|
||||
lastSync: { type: String, required: true },
|
||||
status: { type: String, required: true },
|
||||
auto: { type: Boolean, required: true },
|
||||
interval: { type: String, required: true },
|
||||
builtin: { type: Boolean },
|
||||
}, { versionKey: false });
|
||||
export const EpgSource = model('EpgSource', EpgSourceSchema);
|
||||
//# sourceMappingURL=EpgSource.js.map
|
||||
1
server/dist/models/EpgSource.js.map
vendored
Normal file
1
server/dist/models/EpgSource.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"EpgSource.js","sourceRoot":"","sources":["../../src/models/EpgSource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,eAAe,GAAG,IAAI,MAAM,CAChC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;CAC3B,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC"}
|
||||
18
server/dist/models/Playlist.js
vendored
Normal file
18
server/dist/models/Playlist.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const PlaylistSchema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
name: { type: String, required: true },
|
||||
url: { type: String, required: true },
|
||||
groups: { type: Number, required: true },
|
||||
lastSync: { type: String, required: true },
|
||||
status: { type: String, required: true },
|
||||
auto: { type: Boolean, required: true },
|
||||
interval: { type: String, required: true },
|
||||
builtin: { type: Boolean },
|
||||
// Set for the established (Default) source playlists (dulo/common/dlhd). When present, the
|
||||
// playlist's channels live in the SourceChannel collection (queried by this `source`) instead
|
||||
// of the legacy PlaylistChannel join. Unset for legacy/mock playlists.
|
||||
source: { type: String, default: null, index: true },
|
||||
}, { versionKey: false });
|
||||
export const Playlist = model('Playlist', PlaylistSchema);
|
||||
//# sourceMappingURL=Playlist.js.map
|
||||
1
server/dist/models/Playlist.js.map
vendored
Normal file
1
server/dist/models/Playlist.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Playlist.js","sourceRoot":"","sources":["../../src/models/Playlist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,IAAI,MAAM,CAC/B;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC1B,2FAA2F;IAC3F,8FAA8F;IAC9F,uEAAuE;IACvE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC"}
|
||||
10
server/dist/models/PlaylistChannel.js
vendored
Normal file
10
server/dist/models/PlaylistChannel.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const PlaylistChannelSchema = new Schema({
|
||||
playlistId: { type: String, required: true, index: true },
|
||||
channelId: { type: String, required: true, index: true },
|
||||
order: { type: Number, required: true },
|
||||
}, { versionKey: false });
|
||||
PlaylistChannelSchema.index({ playlistId: 1, channelId: 1 }, { unique: true });
|
||||
PlaylistChannelSchema.index({ playlistId: 1, order: 1 });
|
||||
export const PlaylistChannel = model('PlaylistChannel', PlaylistChannelSchema);
|
||||
//# sourceMappingURL=PlaylistChannel.js.map
|
||||
1
server/dist/models/PlaylistChannel.js.map
vendored
Normal file
1
server/dist/models/PlaylistChannel.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"PlaylistChannel.js","sourceRoot":"","sources":["../../src/models/PlaylistChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,qBAAqB,GAAG,IAAI,MAAM,CACtC;IACE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACzD,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CACxC,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,qBAAqB,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/E,qBAAqB,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC"}
|
||||
11
server/dist/models/Program.js
vendored
Normal file
11
server/dist/models/Program.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const ProgramSchema = new Schema({
|
||||
channelId: { type: String, required: true, index: true },
|
||||
start: { type: Number, required: true },
|
||||
end: { type: Number, required: true },
|
||||
title: { type: String, required: true },
|
||||
cat: { type: String, required: true },
|
||||
}, { versionKey: false });
|
||||
ProgramSchema.index({ channelId: 1, start: 1 });
|
||||
export const Program = model('Program', ProgramSchema);
|
||||
//# sourceMappingURL=Program.js.map
|
||||
1
server/dist/models/Program.js.map
vendored
Normal file
1
server/dist/models/Program.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Program.js","sourceRoot":"","sources":["../../src/models/Program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,aAAa,GAAG,IAAI,MAAM,CAC9B;IACE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CACtC,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,aAAa,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAEhD,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC"}
|
||||
22
server/dist/models/SourceChannel.js
vendored
Normal file
22
server/dist/models/SourceChannel.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const SourceChannelSchema = new Schema({
|
||||
_id: { type: String, required: true },
|
||||
source: { type: String, required: true },
|
||||
sourceChannelId: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
category: { type: String, default: null },
|
||||
groupKey: { type: String, required: true },
|
||||
groupLabel: { type: String, required: true },
|
||||
logoUrl: { type: String, default: null },
|
||||
streamEntryUrl: { type: String, required: true },
|
||||
isPlayable: { type: Boolean, required: true },
|
||||
sourceCreatedAt: { type: String, default: null },
|
||||
sourceUpdatedAt: { type: String, default: null },
|
||||
ingestedAt: { type: String, required: true },
|
||||
}, { versionKey: false });
|
||||
// Covers the per-source grouped/ordered listing query (source → groupKey → name).
|
||||
SourceChannelSchema.index({ source: 1, groupKey: 1, name: 1 });
|
||||
// Dead-channel / playable filtering per source.
|
||||
SourceChannelSchema.index({ source: 1, isPlayable: 1 });
|
||||
export const SourceChannel = model('SourceChannel', SourceChannelSchema);
|
||||
//# sourceMappingURL=SourceChannel.js.map
|
||||
1
server/dist/models/SourceChannel.js.map
vendored
Normal file
1
server/dist/models/SourceChannel.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"SourceChannel.js","sourceRoot":"","sources":["../../src/models/SourceChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAwBzC,MAAM,mBAAmB,GAAG,IAAI,MAAM,CACpC;IACE,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACjD,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACzC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACxC,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAChD,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC7C,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IAChD,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IAChD,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC7C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,kFAAkF;AAClF,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;AAC/D,gDAAgD;AAChD,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;AAExD,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAmB,eAAe,EAAE,mBAAmB,CAAC,CAAC"}
|
||||
11
server/dist/models/StreamSession.js
vendored
Normal file
11
server/dist/models/StreamSession.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
const StreamSessionSchema = new Schema({
|
||||
ip: { type: String, required: true },
|
||||
region: { type: String, required: true },
|
||||
client: { type: String, required: true },
|
||||
joined: { type: String, required: true },
|
||||
bitrate: { type: String, required: true },
|
||||
order: { type: Number, required: true, index: true },
|
||||
}, { versionKey: false });
|
||||
export const StreamSession = model('StreamSession', StreamSessionSchema);
|
||||
//# sourceMappingURL=StreamSession.js.map
|
||||
1
server/dist/models/StreamSession.js.map
vendored
Normal file
1
server/dist/models/StreamSession.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"StreamSession.js","sourceRoot":"","sources":["../../src/models/StreamSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,mBAAmB,GAAG,IAAI,MAAM,CACpC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACpC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC"}
|
||||
13
server/dist/routes/activeStreams.js
vendored
Normal file
13
server/dist/routes/activeStreams.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { ActiveStream } from '../models/ActiveStream.js';
|
||||
export const activeStreamsRouter = Router();
|
||||
activeStreamsRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await ActiveStream.find({}, { _id: 0 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=activeStreams.js.map
|
||||
1
server/dist/routes/activeStreams.js.map
vendored
Normal file
1
server/dist/routes/activeStreams.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"activeStreams.js","sourceRoot":"","sources":["../../src/routes/activeStreams.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,EAAE,CAAC;AAE5C,mBAAmB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACrD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
13
server/dist/routes/activity.js
vendored
Normal file
13
server/dist/routes/activity.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { Activity } from '../models/Activity.js';
|
||||
export const activityRouter = Router();
|
||||
activityRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await Activity.find({}, { _id: 0, order: 0 }).sort({ order: 1 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=activity.js.map
|
||||
1
server/dist/routes/activity.js.map
vendored
Normal file
1
server/dist/routes/activity.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/routes/activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrF,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
22
server/dist/routes/channels.js
vendored
Normal file
22
server/dist/routes/channels.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Router } from 'express';
|
||||
import { Channel } from '../models/Channel.js';
|
||||
import { SourceChannel } from '../models/SourceChannel.js';
|
||||
export const channelsRouter = Router();
|
||||
// GET /api/channels → legacy mock channels (drives the existing dashboard bootstrap)
|
||||
// GET /api/channels?source=<id> → canonical normalized SourceChannel docs for a (Default) source
|
||||
// playlist (the d-combine "path forward" contract, served over Mongo)
|
||||
channelsRouter.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const source = typeof req.query.source === 'string' ? req.query.source : null;
|
||||
if (source) {
|
||||
const docs = await SourceChannel.find({ source }).sort({ groupKey: 1, name: 1 }).lean();
|
||||
return res.json(docs);
|
||||
}
|
||||
const docs = await Channel.find({}, { _id: 0 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=channels.js.map
|
||||
1
server/dist/routes/channels.js.map
vendored
Normal file
1
server/dist/routes/channels.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/routes/channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,gGAAgG;AAChG,iGAAiG;AACjG,sGAAsG;AACtG,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9E,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACxF,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
13
server/dist/routes/customPlaylists.js
vendored
Normal file
13
server/dist/routes/customPlaylists.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { CustomPlaylist } from '../models/CustomPlaylist.js';
|
||||
export const customPlaylistsRouter = Router();
|
||||
customPlaylistsRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await CustomPlaylist.find({}, { _id: 0 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=customPlaylists.js.map
|
||||
1
server/dist/routes/customPlaylists.js.map
vendored
Normal file
1
server/dist/routes/customPlaylists.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"customPlaylists.js","sourceRoot":"","sources":["../../src/routes/customPlaylists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,EAAE,CAAC;AAE9C,qBAAqB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
13
server/dist/routes/epgSources.js
vendored
Normal file
13
server/dist/routes/epgSources.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { EpgSource } from '../models/EpgSource.js';
|
||||
export const epgSourcesRouter = Router();
|
||||
epgSourcesRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await EpgSource.find({}, { _id: 0 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=epgSources.js.map
|
||||
1
server/dist/routes/epgSources.js.map
vendored
Normal file
1
server/dist/routes/epgSources.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"epgSources.js","sourceRoot":"","sources":["../../src/routes/epgSources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,EAAE,CAAC;AAEzC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAClD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACzD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
7
server/dist/routes/health.js
vendored
Normal file
7
server/dist/routes/health.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import { isConnected } from '../db.js';
|
||||
export const healthRouter = Router();
|
||||
healthRouter.get('/', (_req, res) => {
|
||||
res.json({ ok: true, mongo: isConnected() ? 'connected' : 'disconnected' });
|
||||
});
|
||||
//# sourceMappingURL=health.js.map
|
||||
1
server/dist/routes/health.js.map
vendored
Normal file
1
server/dist/routes/health.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC;AAErC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAClC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;AAC9E,CAAC,CAAC,CAAC"}
|
||||
106
server/dist/routes/playlists.js
vendored
Normal file
106
server/dist/routes/playlists.js
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Router } from 'express';
|
||||
import { Playlist } from '../models/Playlist.js';
|
||||
import { PlaylistChannel } from '../models/PlaylistChannel.js';
|
||||
import { Channel } from '../models/Channel.js';
|
||||
import { SourceChannel } from '../models/SourceChannel.js';
|
||||
import { toUiChannel } from '../sources/translate.js';
|
||||
export const playlistsRouter = Router();
|
||||
// Channel count comes from SourceChannel for (Default) source playlists, else the legacy join table.
|
||||
async function channelCountFor(doc) {
|
||||
if (doc.source)
|
||||
return SourceChannel.countDocuments({ source: doc.source });
|
||||
return PlaylistChannel.countDocuments({ playlistId: doc.id });
|
||||
}
|
||||
playlistsRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await Playlist.find({}, { _id: 0 }).lean();
|
||||
const [legacyCounts, sourceCounts] = await Promise.all([
|
||||
PlaylistChannel.aggregate([
|
||||
{ $group: { _id: '$playlistId', count: { $sum: 1 } } },
|
||||
]),
|
||||
SourceChannel.aggregate([
|
||||
{ $group: { _id: '$source', count: { $sum: 1 } } },
|
||||
]),
|
||||
]);
|
||||
const legacyById = new Map(legacyCounts.map((c) => [c._id, c.count]));
|
||||
const sourceBySource = new Map(sourceCounts.map((c) => [c._id, c.count]));
|
||||
res.json(docs.map((d) => ({
|
||||
...d,
|
||||
channels: d.source ? sourceBySource.get(d.source) ?? 0 : legacyById.get(d.id) ?? 0,
|
||||
})));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
playlistsRouter.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const doc = await Playlist.findOne({ id: req.params.id }, { _id: 0 }).lean();
|
||||
if (!doc)
|
||||
return res.status(404).json({ error: 'not_found' });
|
||||
res.json({ ...doc, channels: await channelCountFor(doc) });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// List channels in a playlist. (Default) source playlists project the canonical SourceChannel docs
|
||||
// through the translation layer (UI shape, nulls for unmapped fields); legacy playlists use the join.
|
||||
playlistsRouter.get('/:id/channels', async (req, res, next) => {
|
||||
try {
|
||||
const playlist = await Playlist.findOne({ id: req.params.id }).lean();
|
||||
if (!playlist)
|
||||
return res.status(404).json({ error: 'not_found' });
|
||||
if (playlist.source) {
|
||||
const docs = await SourceChannel.find({ source: playlist.source })
|
||||
.sort({ groupKey: 1, name: 1 })
|
||||
.lean();
|
||||
return res.json(docs.map((d, order) => ({ ...toUiChannel(d), order })));
|
||||
}
|
||||
const memberships = await PlaylistChannel.find({ playlistId: req.params.id }, { _id: 0 })
|
||||
.sort({ order: 1 })
|
||||
.lean();
|
||||
const channelIds = memberships.map((m) => m.channelId);
|
||||
const channels = await Channel.find({ id: { $in: channelIds } }, { _id: 0 }).lean();
|
||||
const byId = new Map(channels.map((c) => [c.id, c]));
|
||||
res.json(memberships
|
||||
.map((m) => {
|
||||
const ch = byId.get(m.channelId);
|
||||
return ch ? { ...ch, order: m.order } : null;
|
||||
})
|
||||
.filter(Boolean));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// Add a channel to a (legacy) playlist (idempotent on the unique pair).
|
||||
playlistsRouter.post('/:id/channels', async (req, res, next) => {
|
||||
try {
|
||||
const { channelId, order } = req.body ?? {};
|
||||
if (typeof channelId !== 'string' || typeof order !== 'number') {
|
||||
return res.status(400).json({ error: 'channelId (string) and order (number) required' });
|
||||
}
|
||||
const doc = await PlaylistChannel.findOneAndUpdate({ playlistId: req.params.id, channelId }, { $set: { order } }, { upsert: true, new: true, projection: { _id: 0 } }).lean();
|
||||
res.status(201).json(doc);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// Remove a channel from a (legacy) playlist.
|
||||
playlistsRouter.delete('/:id/channels/:channelId', async (req, res, next) => {
|
||||
try {
|
||||
const result = await PlaylistChannel.deleteOne({
|
||||
playlistId: req.params.id,
|
||||
channelId: req.params.channelId,
|
||||
});
|
||||
if (result.deletedCount === 0)
|
||||
return res.status(404).json({ error: 'not_found' });
|
||||
res.status(204).end();
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=playlists.js.map
|
||||
1
server/dist/routes/playlists.js.map
vendored
Normal file
1
server/dist/routes/playlists.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
19
server/dist/routes/programs.js
vendored
Normal file
19
server/dist/routes/programs.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import { Program } from '../models/Program.js';
|
||||
export const programsRouter = Router();
|
||||
// All programs, grouped by channelId — matches the EPG_PROGRAMS shape the SPA expects.
|
||||
programsRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await Program.find({}, { _id: 0 }).sort({ channelId: 1, start: 1 }).lean();
|
||||
const grouped = {};
|
||||
for (const d of docs) {
|
||||
const list = grouped[d.channelId] ?? (grouped[d.channelId] = []);
|
||||
list.push({ start: d.start, end: d.end, title: d.title, cat: d.cat });
|
||||
}
|
||||
res.json(grouped);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=programs.js.map
|
||||
1
server/dist/routes/programs.js.map
vendored
Normal file
1
server/dist/routes/programs.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"programs.js","sourceRoot":"","sources":["../../src/routes/programs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,uFAAuF;AACvF,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxF,MAAM,OAAO,GAAsF,EAAE,CAAC;QACtG,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACxE,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
88
server/dist/routes/sources.js
vendored
Normal file
88
server/dist/routes/sources.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
// Generic source RestAPI, ported from ../d-combine/server.mjs into TVApp2's Express stack. One router
|
||||
// serves every source by iterating the registry — adding a source needs zero route changes.
|
||||
//
|
||||
// GET /api/sources manifest (drives the SPA; one entry per registered source)
|
||||
// GET /api/sources/:id/status runtime provenance (dlhd: live mirror; null otherwise)
|
||||
// GET /api/sources/:id/metrics per-source proxy counters
|
||||
// POST /api/sources/:id/sync live refresh → upsert channels + Playlist sync metadata
|
||||
// POST /api/sources/:id/reset restore the committed bundle baseline
|
||||
// GET /api/v1/:source/* single stream proxy; the :source segment binds that source's
|
||||
// resolve+proxy behavior (createProxyHandler per adapter)
|
||||
//
|
||||
// Mounted at the app root (app.use(sourcesRouter)) because its paths span /api/sources, /api/v1, …
|
||||
import { Router } from 'express';
|
||||
import { SOURCES, getSource } from '../sources/registry.js';
|
||||
import { createProxyHandler } from '../sources/core/proxyHandler.js';
|
||||
import { createMetrics, snapshotOne } from '../sources/core/metrics.js';
|
||||
import { syncLive, resetFromBundle } from '../sources/seed.js';
|
||||
export const sourcesRouter = Router();
|
||||
// Build one proxy handler (+ metrics bag) per source once, then dispatch by the :source segment.
|
||||
const metricsById = new Map();
|
||||
const proxyHandlers = new Map();
|
||||
for (const adapter of SOURCES) {
|
||||
const m = createMetrics();
|
||||
metricsById.set(adapter.id, m);
|
||||
proxyHandlers.set(adapter.id, createProxyHandler(adapter, m));
|
||||
}
|
||||
// ── Manifest ────────────────────────────────────────────────────────────────
|
||||
sourcesRouter.get('/api/sources', (_req, res) => {
|
||||
res.json(SOURCES.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
grouping: s.grouping,
|
||||
sourceUrl: `/api/channels?source=${s.id}`, // normalized catalog over Mongo
|
||||
proxyPrefix: `/api/v1/${s.id}/`,
|
||||
statusUrl: s.status ? `/api/sources/${s.id}/status` : null,
|
||||
})));
|
||||
});
|
||||
// ── Per-source runtime status (dlhd mirror provenance; null for sources without one) ──
|
||||
sourcesRouter.get('/api/sources/:id/status', async (req, res, next) => {
|
||||
try {
|
||||
const adapter = getSource(req.params.id);
|
||||
if (!adapter)
|
||||
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
|
||||
const status = adapter.status ? await adapter.status() : null;
|
||||
res.json(status ?? null);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ── Per-source proxy metrics ──────────────────────────────────────────────────
|
||||
sourcesRouter.get('/api/sources/:id/metrics', (req, res) => {
|
||||
const m = metricsById.get(req.params.id);
|
||||
if (!m)
|
||||
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
|
||||
res.json(snapshotOne(m));
|
||||
});
|
||||
// ── Live sync (refresh channels + Playlist sync metadata from upstream) ───────
|
||||
sourcesRouter.post('/api/sources/:id/sync', async (req, res, next) => {
|
||||
try {
|
||||
if (!getSource(req.params.id))
|
||||
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
|
||||
res.json(await syncLive(req.params.id));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ── Reset to the committed bundle baseline ────────────────────────────────────
|
||||
sourcesRouter.post('/api/sources/:id/reset', async (req, res, next) => {
|
||||
try {
|
||||
if (!getSource(req.params.id))
|
||||
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
|
||||
res.json(await resetFromBundle(req.params.id));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ── Single stream proxy API ───────────────────────────────────────────────────
|
||||
sourcesRouter.get('/api/v1/:source/*', (req, res) => {
|
||||
const handler = proxyHandlers.get(req.params.source);
|
||||
if (!handler) {
|
||||
return res.status(404).type('text/plain').send(`Unknown source: ${req.params.source}`);
|
||||
}
|
||||
return handler(req, res, () => undefined);
|
||||
});
|
||||
//# sourceMappingURL=sources.js.map
|
||||
1
server/dist/routes/sources.js.map
vendored
Normal file
1
server/dist/routes/sources.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/routes/sources.ts"],"names":[],"mappings":"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"}
|
||||
13
server/dist/routes/streamSessions.js
vendored
Normal file
13
server/dist/routes/streamSessions.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { StreamSession } from '../models/StreamSession.js';
|
||||
export const streamSessionsRouter = Router();
|
||||
streamSessionsRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const docs = await StreamSession.find({}, { _id: 0, order: 0 }).sort({ order: 1 }).lean();
|
||||
res.json(docs);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=streamSessions.js.map
|
||||
1
server/dist/routes/streamSessions.js.map
vendored
Normal file
1
server/dist/routes/streamSessions.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"streamSessions.js","sourceRoot":"","sources":["../../src/routes/streamSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,EAAE,CAAC;AAE7C,oBAAoB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
||||
152
server/dist/seed.js
vendored
Normal file
152
server/dist/seed.js
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
// Seed script — populates every collection from inlined mock data.
|
||||
// Idempotent: drops and refills each collection.
|
||||
//
|
||||
// cd server && npm run seed
|
||||
//
|
||||
// Uses the same config resolution as the API (TVAPP2_CONFIG or ./config.local.json).
|
||||
import { loadConfig } from './config.js';
|
||||
import { connect, disconnect } from './db.js';
|
||||
import { Playlist } from './models/Playlist.js';
|
||||
import { Channel } from './models/Channel.js';
|
||||
import { PlaylistChannel } from './models/PlaylistChannel.js';
|
||||
import { EpgSource } from './models/EpgSource.js';
|
||||
import { CustomPlaylist } from './models/CustomPlaylist.js';
|
||||
import { ActiveStream } from './models/ActiveStream.js';
|
||||
import { Program } from './models/Program.js';
|
||||
import { Activity } from './models/Activity.js';
|
||||
import { StreamSession } from './models/StreamSession.js';
|
||||
const PLAYLISTS = [
|
||||
{ id: 'pl-default', name: 'Default', url: 'bundled://tvapp2/default.m3u', groups: 6, lastSync: 'Ships with TVApp2', status: 'good', auto: true, interval: 'Auto-updated', builtin: true },
|
||||
{ id: 'pl-iptv-pro', name: 'IPTV-Pro Main', url: 'https://iptv-pro.example.com/playlist.m3u8', groups: 8, lastSync: '2 minutes ago', status: 'good', auto: true, interval: 'Every 6 hours' },
|
||||
{ id: 'pl-free-uk', name: 'Free UK Bouquet', url: 'https://iptv-org.github.io/iptv/countries/uk.m3u', groups: 4, lastSync: '1 hour ago', status: 'good', auto: true, interval: 'Daily' },
|
||||
{ id: 'pl-archive', name: 'Archive (legacy)', url: 'file:///playlists/archive-2023.m3u', groups: 3, lastSync: '3 days ago', status: 'warn', auto: false, interval: 'Manual' },
|
||||
];
|
||||
const EPG_SOURCES = [
|
||||
{ id: 'epg-default', name: 'Default', url: 'bundled://tvapp2/default.xml.gz', channels: 86, programs: 5240, lastSync: 'Ships with TVApp2', status: 'good', auto: true, interval: 'Auto-updated', builtin: true },
|
||||
{ id: 'epg-xmltv-uk', name: 'XMLTV UK Guide', url: 'https://epg.example.com/uk.xml.gz', channels: 124, programs: 8420, lastSync: '12 minutes ago', status: 'good', auto: true, interval: 'Every 12 hours' },
|
||||
{ id: 'epg-iptv-org', name: 'iptv-org world EPG', url: 'https://iptv-org.github.io/epg/guides/uk/openepg.xml', channels: 412, programs: 24180, lastSync: '2 hours ago', status: 'good', auto: true, interval: 'Daily' },
|
||||
];
|
||||
const CHANNEL_SEEDS = [
|
||||
{ tvg_name: 'BBC One HD', group: 'Entertainment', channel: 101, tvg_id: 'bbc.one.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'BBC Two HD', group: 'Entertainment', channel: 102, tvg_id: 'bbc.two.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'BBC News', group: 'News', channel: 231, tvg_id: 'bbc.news.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Sky Sports Main', group: 'Sport', channel: 401, tvg_id: 'sky.sports.main.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'Sky Sports F1', group: 'Sport', channel: 406, tvg_id: 'sky.sports.f1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'ITV1 HD', group: 'Entertainment', channel: 103, tvg_id: 'itv1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'Channel 4 HD', group: 'Entertainment', channel: 104, tvg_id: 'channel4.uk', state: 'active', epg: 'matched', status: 'warn', res: '1080p' },
|
||||
{ tvg_name: 'Film4', group: 'Movies', channel: 315, tvg_id: 'film4.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Discovery Channel', group: 'Documentary', channel: 520, tvg_id: 'discovery.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'National Geographic', group: 'Documentary', channel: 521, tvg_id: 'natgeo.uk', state: 'active', epg: 'unmatched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'CNN International', group: 'News', channel: 233, tvg_id: 'cnn.int', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Al Jazeera English', group: 'News', channel: 235, tvg_id: 'aljazeera.en', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Cartoon Network', group: 'Kids', channel: 601, tvg_id: 'cartoonnet.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Nick Jr.', group: 'Kids', channel: 615, tvg_id: null, state: 'disabled', epg: 'unmatched', status: 'warn', res: '720p' },
|
||||
{ tvg_name: 'MTV Hits', group: 'Music', channel: 365, tvg_id: 'mtv.hits.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Kerrang!', group: 'Music', channel: 369, tvg_id: 'kerrang.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'Food Network', group: 'Lifestyle', channel: 240, tvg_id: 'foodnet.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
|
||||
{ tvg_name: 'HGTV', group: 'Lifestyle', channel: 242, tvg_id: null, state: 'disabled', epg: 'unmatched', status: 'bad', res: '720p' },
|
||||
{ tvg_name: 'TCM Movies', group: 'Movies', channel: 320, tvg_id: 'tcm.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
{ tvg_name: 'Eurosport 1', group: 'Sport', channel: 410, tvg_id: 'eurosport1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
|
||||
];
|
||||
const CHANNELS = CHANNEL_SEEDS.map((c, i) => ({
|
||||
id: `ch-${i}`,
|
||||
...c,
|
||||
source: 'Default',
|
||||
url: `http://sample.stream.com/channel/1.m3u8?ch=ch-${i}`,
|
||||
logoColor: `oklch(0.5 0.16 ${(i * 47) % 360})`,
|
||||
initials: c.tvg_name.split(/\s+/).slice(0, 2).map((w) => w[0]).join('').toUpperCase(),
|
||||
}));
|
||||
const CUSTOM_PLAYLISTS = [
|
||||
{ id: 'cust-sports-night', name: 'Sports Night', slug: 'sports-night', channels: 12, updated: '2 days ago' },
|
||||
{ id: 'cust-kids-safe', name: 'Kids Safe', slug: 'kids-safe', channels: 8, updated: 'yesterday' },
|
||||
{ id: 'cust-news-rotation', name: 'News Rotation', slug: 'news-rotation', channels: 6, updated: '5 hours ago' },
|
||||
{ id: 'cust-living-room', name: 'Living Room Favorites', slug: 'living-room', channels: 18, updated: '1 week ago' },
|
||||
];
|
||||
const ACTIVE_STREAMS = [
|
||||
{ id: 'as-1', channelId: 'ch-0', status: 'good', uptime: '4h 12m', uptimeMin: 252, viewers: 142, peakViewers: 168, bitrate: 6.4, targetBitrate: 6.0, codec: 'H.264 High@4.1', audio: 'AAC LC 2.0 · 128k', container: 'HLS / TS', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/bbc-one/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 0, droppedRatio: 0.00, latency: 2.1, bandwidth: 912 },
|
||||
{ id: 'as-2', channelId: 'ch-3', status: 'good', uptime: '1h 48m', uptimeMin: 108, viewers: 89, peakViewers: 112, bitrate: 8.2, targetBitrate: 8.0, codec: 'H.264 High@4.2', audio: 'AC3 5.1 · 384k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/sky-sports-main/index.m3u8', sourceHost: 'edge-lon-02', droppedFrames: 14, droppedRatio: 0.01, latency: 1.8, bandwidth: 730 },
|
||||
{ id: 'as-3', channelId: 'ch-2', status: 'good', uptime: '22h 06m', uptimeMin: 1326, viewers: 47, peakViewers: 61, bitrate: 3.1, targetBitrate: 3.0, codec: 'H.264 Main@3.1', audio: 'AAC LC 2.0 · 96k', container: 'HLS / TS', resolution: '1280×720', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/bbc-news/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 2, droppedRatio: 0.00, latency: 2.4, bandwidth: 145 },
|
||||
{ id: 'as-4', channelId: 'ch-6', status: 'warn', uptime: '12m', uptimeMin: 12, viewers: 8, peakViewers: 8, bitrate: 4.1, targetBitrate: 5.0, codec: 'H.264 High@4.0', audio: 'AAC LC 2.0 · 128k', container: 'HLS / TS', resolution: '1920×1080', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/channel4/index.m3u8', sourceHost: 'edge-ams-01', droppedFrames: 184, droppedRatio: 0.47, latency: 4.7, bandwidth: 34 },
|
||||
{ id: 'as-5', channelId: 'ch-4', status: 'good', uptime: '3h 41m', uptimeMin: 221, viewers: 31, peakViewers: 44, bitrate: 7.9, targetBitrate: 8.0, codec: 'H.264 High@4.2', audio: 'AC3 5.1 · 384k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/sky-sports-f1/index.m3u8', sourceHost: 'edge-lon-02', droppedFrames: 0, droppedRatio: 0.00, latency: 1.9, bandwidth: 245 },
|
||||
{ id: 'as-6', channelId: 'ch-8', status: 'good', uptime: '45m', uptimeMin: 45, viewers: 12, peakViewers: 12, bitrate: 5.6, targetBitrate: 6.0, codec: 'H.265 Main@4.0', audio: 'AAC LC 2.0 · 128k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/discovery/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 1, droppedRatio: 0.00, latency: 2.2, bandwidth: 68 },
|
||||
{ id: 'as-7', channelId: 'ch-17', status: 'bad', uptime: '—', uptimeMin: 0, viewers: 0, peakViewers: 4, bitrate: 0, targetBitrate: 5.0, codec: '—', audio: '—', container: 'HLS / TS', resolution: '—', fps: 0, sourceUrl: 'http://stream.iptv-pro.example.com/live/hgtv/index.m3u8', sourceHost: 'edge-ams-01', droppedFrames: 0, droppedRatio: 0, latency: 0, bandwidth: 0 },
|
||||
];
|
||||
const ACTIVITY = [
|
||||
{ when: '2m', icon: 'sync', html: '<b>IPTV-Pro Main</b> synced — 142 channels, no changes' },
|
||||
{ when: '12m', icon: 'epg', html: '<b>XMLTV UK Guide</b> imported — 8,420 programs across 124 channels' },
|
||||
{ when: '1h', icon: 'map', html: 'Manual mapping: <b>HGTV</b> → <code class="mono">hgtv.uk</code>' },
|
||||
{ when: '1h', icon: 'warn', html: '<b>Free UK Bouquet</b> reports 3 channels offline (HTTP 503)' },
|
||||
{ when: '3h', icon: 'edit', html: 'Renamed <b>Discovery</b> → <b>Discovery Channel</b>' },
|
||||
{ when: 'Yest.', icon: 'add', html: 'Playlist <b>IPTV-Pro Main</b> added (142 channels)' },
|
||||
];
|
||||
const STREAM_SESSIONS = [
|
||||
{ ip: '82.14.221.47', region: 'GB · London', client: 'VLC / Linux', joined: '2m ago', bitrate: '6.4 Mbps' },
|
||||
{ ip: '192.81.45.12', region: 'DE · Frankfurt', client: 'Tivimate / Android TV', joined: '8m ago', bitrate: '6.4 Mbps' },
|
||||
{ ip: '104.18.92.5', region: 'NL · Amsterdam', client: 'OTT Navigator / FireTV', joined: '14m ago', bitrate: '3.1 Mbps' },
|
||||
{ ip: '176.58.103.9', region: 'GB · Manchester', client: 'Kodi 21', joined: '31m ago', bitrate: '6.4 Mbps' },
|
||||
{ ip: '78.143.211.4', region: 'FR · Paris', client: 'IPTV Smarters / iOS', joined: '1h ago', bitrate: '3.1 Mbps' },
|
||||
{ ip: '10.0.4.118', region: 'Local · LAN', client: 'ffmpeg / probe', joined: '3h ago', bitrate: '6.4 Mbps' },
|
||||
];
|
||||
const PROGRAM_LIBRARY = [
|
||||
['Morning News', 'Live'], ['Breakfast Show', 'Lifestyle'], ['Market Report', 'Business'],
|
||||
['Sports Roundup', 'Highlights'], ['Drama Hour', 'Series'], ['World Headlines', 'News'],
|
||||
['Wildlife Special', 'Documentary'], ['Cooking with Anna', 'Lifestyle'],
|
||||
['Classic Movies', 'Film'], ['Talk of the Day', 'Discussion'], ["Children's Hour", 'Kids'],
|
||||
['Weather Watch', 'Weather'], ['Live Match', 'Football'], ['Tech Today', 'Technology'],
|
||||
['Late Show', 'Comedy'], ['Documentary', 'Feature'], ['Music Mix', 'Music'],
|
||||
['Quiz Night', 'Game show'], ['Reality TV', 'Series'], ['The Daily Brief', 'News'],
|
||||
];
|
||||
function rngFor(seed) {
|
||||
let s = seed;
|
||||
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
||||
}
|
||||
function generatePrograms(channelId, seedBase) {
|
||||
const rng = rngFor(seedBase);
|
||||
const progs = [];
|
||||
let t = 0;
|
||||
while (t < 24) {
|
||||
const dur = [0.5, 1, 1, 1.5, 2][Math.floor(rng() * 5)];
|
||||
const idx = Math.floor(rng() * PROGRAM_LIBRARY.length);
|
||||
progs.push({ channelId, start: t, end: Math.min(24, t + dur), title: PROGRAM_LIBRARY[idx][0], cat: PROGRAM_LIBRARY[idx][1] });
|
||||
t += dur;
|
||||
}
|
||||
return progs;
|
||||
}
|
||||
async function main() {
|
||||
const config = loadConfig();
|
||||
await connect(config.mongoUri);
|
||||
await Promise.all([
|
||||
Playlist.deleteMany({}),
|
||||
Channel.deleteMany({}),
|
||||
PlaylistChannel.deleteMany({}),
|
||||
EpgSource.deleteMany({}),
|
||||
CustomPlaylist.deleteMany({}),
|
||||
ActiveStream.deleteMany({}),
|
||||
Program.deleteMany({}),
|
||||
Activity.deleteMany({}),
|
||||
StreamSession.deleteMany({}),
|
||||
]);
|
||||
await Playlist.insertMany(PLAYLISTS);
|
||||
await Channel.insertMany(CHANNELS);
|
||||
await EpgSource.insertMany(EPG_SOURCES);
|
||||
await CustomPlaylist.insertMany(CUSTOM_PLAYLISTS);
|
||||
await ActiveStream.insertMany(ACTIVE_STREAMS);
|
||||
// Default playlist holds every channel in order.
|
||||
const defaultPlaylist = PLAYLISTS.find((p) => p.builtin) ?? PLAYLISTS[0];
|
||||
await PlaylistChannel.insertMany(CHANNELS.map((c, order) => ({ playlistId: defaultPlaylist.id, channelId: c.id, order })));
|
||||
// EPG programs for the first 12 channels (matches the original mock).
|
||||
const programs = CHANNELS.slice(0, 12).flatMap((c, i) => generatePrograms(c.id, 100 + i * 7));
|
||||
await Program.insertMany(programs);
|
||||
await Activity.insertMany(ACTIVITY.map((a, order) => ({ ...a, order })));
|
||||
await StreamSession.insertMany(STREAM_SESSIONS.map((s, order) => ({ ...s, order })));
|
||||
console.info(`[seed] playlists=${PLAYLISTS.length} channels=${CHANNELS.length} ` +
|
||||
`epg-sources=${EPG_SOURCES.length} custom-playlists=${CUSTOM_PLAYLISTS.length} ` +
|
||||
`active-streams=${ACTIVE_STREAMS.length} programs=${programs.length} ` +
|
||||
`activity=${ACTIVITY.length} stream-sessions=${STREAM_SESSIONS.length}`);
|
||||
await disconnect();
|
||||
}
|
||||
main().catch((err) => {
|
||||
console.error('[seed] failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=seed.js.map
|
||||
1
server/dist/seed.js.map
vendored
Normal file
1
server/dist/seed.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
116
server/dist/sources/adapters/dulo.js
vendored
Normal file
116
server/dist/sources/adapters/dulo.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
// dulo.tv source adapter (Phase 1). Ported from ../d-combine/sources/dulo/adapter.mjs.
|
||||
//
|
||||
// dulo.tv exposes a JSON catalog API and each channel's `source_url` IS a token-free HLS master
|
||||
// playlist. The memfs hosts gate playback behind an Origin allowlist, so the proxy injects
|
||||
// `Origin: https://dulo.tv` on every hop. No server-side resolve is needed.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { snapshotFile } from '../paths.js';
|
||||
const SNAPSHOT = snapshotFile('dulo');
|
||||
const DULO_ORIGIN = 'https://dulo.tv';
|
||||
const DULO_API = process.env.DULO_API || 'https://dulo.tv/api/live-tv/channels';
|
||||
function isHttpUrl(url) {
|
||||
if (typeof url !== 'string')
|
||||
return false;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return u.protocol === 'https:' || u.protocol === 'http:';
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function toIso(ts) {
|
||||
if (!ts || typeof ts !== 'string')
|
||||
return null;
|
||||
const d = new Date(ts);
|
||||
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
||||
}
|
||||
const duloAdapter = {
|
||||
id: 'dulo',
|
||||
label: 'dulo',
|
||||
// Prefer the live catalog API; fall back to the captured snapshot when offline / region-blocked.
|
||||
async listChannels() {
|
||||
try {
|
||||
const res = await fetch(DULO_API, { headers: { Origin: DULO_ORIGIN } });
|
||||
if (!res.ok)
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
const body = (await res.json());
|
||||
const raw = body.channels || [];
|
||||
if (!raw.length)
|
||||
throw new Error('empty channel list');
|
||||
return { raw, meta: { endpoint: DULO_API, live: true, fetchedAt: new Date().toISOString() } };
|
||||
}
|
||||
catch (err) {
|
||||
const snap = JSON.parse(readFileSync(SNAPSHOT, 'utf8'));
|
||||
return {
|
||||
raw: snap.channels || [],
|
||||
meta: {
|
||||
endpoint: DULO_API,
|
||||
live: false,
|
||||
fallback: 'dulo.snapshot.json',
|
||||
reason: err.message,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
normalize(raw, { ingestedAt }) {
|
||||
const sourceChannelId = String(raw.id);
|
||||
const category = raw.category || null;
|
||||
return {
|
||||
_id: `dulo:${sourceChannelId}`,
|
||||
source: 'dulo',
|
||||
sourceChannelId,
|
||||
name: raw.name,
|
||||
category, // dulo has real semantic categories
|
||||
groupKey: category || 'uncategorized',
|
||||
groupLabel: category || 'uncategorized',
|
||||
logoUrl: raw.logo_url || null,
|
||||
streamEntryUrl: raw.source_url, // token-free master .m3u8 (handed straight to the proxy)
|
||||
isPlayable: isHttpUrl(raw.source_url),
|
||||
sourceCreatedAt: toIso(raw.created_at),
|
||||
sourceUpdatedAt: toIso(raw.updated_at),
|
||||
ingestedAt,
|
||||
};
|
||||
},
|
||||
grouping: { by: 'groupKey', groupOrder: 'alpha', channelOrder: 'name' },
|
||||
isEntryUrl() {
|
||||
return false; // dulo source_url is already the master — nothing to resolve
|
||||
},
|
||||
async resolveStream(entryUrl) {
|
||||
return { masterUrl: entryUrl }; // identity no-op
|
||||
},
|
||||
proxy: {
|
||||
upstreamHeaders() {
|
||||
return { Origin: DULO_ORIGIN }; // the memfs Origin allowlist gate
|
||||
},
|
||||
isAllowedUpstream(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return (u.protocol === 'https:' || u.protocol === 'http:') && u.hostname.endsWith('.dulo.tv');
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onPlaylistChildHost: null, // static allowlist — nothing to learn at runtime
|
||||
relabelSegmentContentType(_url, contentType) {
|
||||
return contentType || 'application/octet-stream'; // plain TS — pass the upstream type through
|
||||
},
|
||||
classifyArtifact(url) {
|
||||
try {
|
||||
const p = new URL(url).pathname.toLowerCase();
|
||||
if (p.endsWith('.ts'))
|
||||
return 'segment';
|
||||
if (p.endsWith('.m3u8'))
|
||||
return p.includes('_output_') ? 'variant' : 'master';
|
||||
return 'other';
|
||||
}
|
||||
catch {
|
||||
return 'other';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
export default duloAdapter;
|
||||
//# sourceMappingURL=dulo.js.map
|
||||
1
server/dist/sources/adapters/dulo.js.map
vendored
Normal file
1
server/dist/sources/adapters/dulo.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dulo.js","sourceRoot":"","sources":["../../../src/sources/adapters/dulo.ts"],"names":[],"mappings":"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"}
|
||||
39
server/dist/sources/core/buildSource.js
vendored
Normal file
39
server/dist/sources/core/buildSource.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// The generic "standard function" pipeline, run once per source for a LIVE sync:
|
||||
// listChannels() → raw upstream records → normalize(raw) → one SourceChannel doc each
|
||||
// → dedupe by deterministic _id (last wins, idempotent) → return docs for upsert.
|
||||
//
|
||||
// Ported from d-combine/lib/core/build-source.mjs, but returns the docs instead of writing files —
|
||||
// the seed module upserts them into Mongo. The whole file is source-agnostic.
|
||||
import { logger } from './logger.js';
|
||||
export async function buildSource(adapter) {
|
||||
const startedAt = Date.now();
|
||||
logger.info('build', `[${adapter.id}] fetching channel listings…`);
|
||||
const { raw, meta = {} } = await adapter.listChannels();
|
||||
logger.info('build', `[${adapter.id}] got ${raw.length} raw records (${meta.live === false ? 'offline snapshot' : 'live'})`);
|
||||
const ingestedAt = new Date().toISOString();
|
||||
const normalized = [];
|
||||
for (const record of raw) {
|
||||
try {
|
||||
const doc = adapter.normalize(record, { ingestedAt });
|
||||
if (doc)
|
||||
normalized.push(doc);
|
||||
}
|
||||
catch (err) {
|
||||
logger.warn('build', `[${adapter.id}] skipped a record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
// Dedupe by deterministic _id (last wins) — guards against duplicate upstream rows.
|
||||
const byId = new Map();
|
||||
for (const doc of normalized)
|
||||
byId.set(doc._id, doc);
|
||||
const docs = [...byId.values()];
|
||||
logger.ok('build', `[${adapter.id}] normalized ${docs.length} docs`);
|
||||
return {
|
||||
id: adapter.id,
|
||||
count: docs.length,
|
||||
docs,
|
||||
live: meta.live !== false,
|
||||
meta: { ...meta, buildMs: Date.now() - startedAt },
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=buildSource.js.map
|
||||
1
server/dist/sources/core/buildSource.js.map
vendored
Normal file
1
server/dist/sources/core/buildSource.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"buildSource.js","sourceRoot":"","sources":["../../../src/sources/core/buildSource.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,yFAAyF;AACzF,oFAAoF;AACpF,EAAE;AACF,mGAAmG;AACnG,8EAA8E;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAYrC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAsB;IACtD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,8BAA8B,CAAC,CAAC;IAEnE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IACxD,MAAM,CAAC,IAAI,CACT,OAAO,EACP,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,MAAM,iBAAiB,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,GAAG,CACvG,CAAC;IAEF,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,UAAU,GAAuB,EAAE,CAAC;IAC1C,KAAK,MAAM,MAAM,IAAI,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;YACtD,IAAI,GAAG;gBAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,uBAAwB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;IACjD,KAAK,MAAM,GAAG,IAAI,UAAU;QAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAEhC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,gBAAgB,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;IACrE,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,KAAK,EAAE,IAAI,CAAC,MAAM;QAClB,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,KAAK;QACzB,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE;KACnD,CAAC;AACJ,CAAC"}
|
||||
18
server/dist/sources/core/logger.js
vendored
Normal file
18
server/dist/sources/core/logger.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// Tiny tagged logger matching the existing console style ("[mongo] connected", "[api] …").
|
||||
// Ported from d-combine/lib/core/logger.mjs, trimmed to what the core uses.
|
||||
function emit(level, tag, msg) {
|
||||
const line = `[${tag}] ${msg}`;
|
||||
if (level === 'error')
|
||||
console.error(line);
|
||||
else if (level === 'warn')
|
||||
console.warn(line);
|
||||
else
|
||||
console.info(line);
|
||||
}
|
||||
export const logger = {
|
||||
info: (tag, msg) => emit('info', tag, msg),
|
||||
warn: (tag, msg) => emit('warn', tag, msg),
|
||||
error: (tag, msg) => emit('error', tag, msg),
|
||||
ok: (tag, msg) => emit('ok', tag, msg),
|
||||
};
|
||||
//# sourceMappingURL=logger.js.map
|
||||
1
server/dist/sources/core/logger.js.map
vendored
Normal file
1
server/dist/sources/core/logger.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../src/sources/core/logger.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,4EAA4E;AAI5E,SAAS,IAAI,CAAC,KAAY,EAAE,GAAW,EAAE,GAAW;IAClD,MAAM,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;IAC/B,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SACtC,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;QACzC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC;IAC1D,IAAI,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC;IAC1D,KAAK,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC;IAC5D,EAAE,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC;CACvD,CAAC"}
|
||||
35
server/dist/sources/core/metrics.js
vendored
Normal file
35
server/dist/sources/core/metrics.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// In-memory counters, one set PER SOURCE. Surfaced via /api/sources/:id/status and /health-style
|
||||
// reporting. Ported from d-combine/lib/core/metrics.mjs.
|
||||
export function createMetrics() {
|
||||
return {
|
||||
startedAt: Date.now(),
|
||||
requests: { total: 0, master: 0, variant: 0, segment: 0, other: 0, errors: 0 },
|
||||
upstream: { ok: 0, notLive: 0, forbidden: 0, failed: 0 }, // 404=notLive, 403=forbidden(gate)
|
||||
bytesStreamed: 0,
|
||||
active: 0,
|
||||
lastStreamAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
}
|
||||
/** Human-readable byte size. */
|
||||
export function fmtBytes(n) {
|
||||
if (n < 1024)
|
||||
return `${n}B`;
|
||||
if (n < 1048576)
|
||||
return `${(n / 1024).toFixed(1)}KB`;
|
||||
return `${(n / 1048576).toFixed(2)}MB`;
|
||||
}
|
||||
/** JSON snapshot of one source's metrics. */
|
||||
export function snapshotOne(m) {
|
||||
return {
|
||||
uptimeSeconds: Math.round((Date.now() - m.startedAt) / 1000),
|
||||
active: m.active,
|
||||
requests: { ...m.requests },
|
||||
upstream: { ...m.upstream },
|
||||
bytesStreamed: m.bytesStreamed,
|
||||
mbStreamed: +(m.bytesStreamed / 1048576).toFixed(2),
|
||||
lastStreamAt: m.lastStreamAt ? new Date(m.lastStreamAt).toISOString() : null,
|
||||
lastError: m.lastError,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=metrics.js.map
|
||||
1
server/dist/sources/core/metrics.js.map
vendored
Normal file
1
server/dist/sources/core/metrics.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/sources/core/metrics.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,yDAAyD;AAmBzD,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;QAC9E,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,mCAAmC;QAC7F,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,CAAC;QACT,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;KAChB,CAAC;AACJ,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,IAAI,CAAC,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC;IAC7B,IAAI,CAAC,GAAG,OAAO;QAAE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,OAAO,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,OAAO;QACL,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QAC5D,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC3B,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC3B,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACnD,YAAY,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QAC5E,SAAS,EAAE,CAAC,CAAC,SAAS;KACvB,CAAC;AACJ,CAAC"}
|
||||
43
server/dist/sources/core/playlist.js
vendored
Normal file
43
server/dist/sources/core/playlist.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Shared HLS playlist helpers. Ported from d-combine/lib/core/playlist.mjs. The only per-source
|
||||
// difference — whether the rewriter also learns (allowlists) each child host — is the `onChildHost`
|
||||
// hook param, so one implementation serves every source.
|
||||
/** True if the upstream URL / content-type looks like an HLS playlist (.m3u8). */
|
||||
export function looksLikePlaylist(upstreamUrl, contentType) {
|
||||
if (contentType && contentType.includes('mpegurl'))
|
||||
return true; // apple.mpegurl / x-mpegurl
|
||||
try {
|
||||
return new URL(upstreamUrl).pathname.toLowerCase().endsWith('.m3u8');
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Rewrite every child URI in a playlist so it routes back through this proxy.
|
||||
*
|
||||
* @param text the raw playlist body
|
||||
* @param baseUrl the upstream URL it was fetched from (for relative→absolute)
|
||||
* @param prefix proxy mount prefix to prepend, e.g. "/api/v1/dulo/"
|
||||
* @param onChildHost per-child-host hook (dlhd dynamic-allow; dulo/common null)
|
||||
*/
|
||||
export function rewritePlaylist(text, baseUrl, prefix, onChildHost) {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((rawLine) => {
|
||||
const trimmed = rawLine.trim();
|
||||
if (!trimmed || trimmed.startsWith('#'))
|
||||
return rawLine; // tag / comment / blank → as-is
|
||||
const abs = new URL(trimmed, baseUrl).href; // resolve relative → absolute
|
||||
if (onChildHost) {
|
||||
try {
|
||||
onChildHost(new URL(abs).hostname);
|
||||
}
|
||||
catch {
|
||||
/* ignore malformed */
|
||||
}
|
||||
}
|
||||
return `${prefix}${encodeURIComponent(abs)}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
//# sourceMappingURL=playlist.js.map
|
||||
1
server/dist/sources/core/playlist.js.map
vendored
Normal file
1
server/dist/sources/core/playlist.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"playlist.js","sourceRoot":"","sources":["../../../src/sources/core/playlist.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,oGAAoG;AACpG,yDAAyD;AAEzD,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,WAAmB,EAAE,WAAmB;IACxE,IAAI,WAAW,IAAI,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,4BAA4B;IAC7F,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,OAAe,EACf,MAAc,EACd,WAA4C;IAE5C,OAAO,IAAI;SACR,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACf,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,CAAC,gCAAgC;QACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,8BAA8B;QAC1E,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,WAAW,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;QACD,OAAO,GAAG,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/C,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
|
||||
154
server/dist/sources/core/proxyHandler.js
vendored
Normal file
154
server/dist/sources/core/proxyHandler.js
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
// One Express handler factory per source, bound to GET /api/v1/:source/*. Ported from
|
||||
// d-combine/lib/core/proxy-handler.mjs. Every per-source difference (resolve, headers, SSRF allow,
|
||||
// dynamic-allow, segment relabel, artifact classification) is read off `adapter`; the control flow
|
||||
// below is invariant.
|
||||
//
|
||||
// /api/v1/<source>/<enc entry-or-stream URL>
|
||||
// · entry URL (dlhd watch.php / stream-N.php) → adapter.resolveStream() → fresh master, then proxy
|
||||
// · master/variant .m3u8 → rewrite child URIs back through /api/v1/<source>/…
|
||||
// · segment → pipe bytes (adapter may relabel the content-type)
|
||||
import { Readable } from 'node:stream';
|
||||
import { logger } from './logger.js';
|
||||
import { fmtBytes } from './metrics.js';
|
||||
import { looksLikePlaylist, rewritePlaylist } from './playlist.js';
|
||||
function label(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const file = u.pathname.split('/').pop() || '';
|
||||
return { host: u.hostname, short: file.slice(0, 8) || '/' };
|
||||
}
|
||||
catch {
|
||||
return { host: '?', short: '?' };
|
||||
}
|
||||
}
|
||||
export function createProxyHandler(adapter, metrics) {
|
||||
// Marker used to slice the raw (still-encoded) upstream URL out of req.originalUrl, independent of
|
||||
// where the router is mounted. Keeps embedded ?session=/?md5&expires through ONE decodeURIComponent.
|
||||
const MARKER = `/v1/${adapter.id}/`;
|
||||
const PREFIX = `/api/v1/${adapter.id}/`;
|
||||
const tag = `stream:${adapter.id}`;
|
||||
const { proxy } = adapter;
|
||||
return async function handler(req, res) {
|
||||
const startedAt = Date.now();
|
||||
const ms = () => `${Date.now() - startedAt}ms`;
|
||||
// 1. Extract + decode the single percent-encoded upstream URL segment.
|
||||
const idx = req.originalUrl.indexOf(MARKER);
|
||||
const rawPath = idx >= 0 ? req.originalUrl.slice(idx + MARKER.length) : '';
|
||||
let upstreamUrl;
|
||||
try {
|
||||
upstreamUrl = decodeURIComponent(rawPath);
|
||||
}
|
||||
catch {
|
||||
logger.warn(tag, `400 malformed encoded URL from ${req.ip}`);
|
||||
res.status(400).type('text/plain').send('Bad request: malformed encoded URL');
|
||||
return;
|
||||
}
|
||||
// 2. Resolve-then-proxy: an entry URL must become a fresh stream URL first
|
||||
// (dulo/common: never — entry IS the master; dlhd: the 3-hop scrape).
|
||||
if (adapter.isEntryUrl(upstreamUrl)) {
|
||||
metrics.requests.total++;
|
||||
metrics.requests.master++;
|
||||
try {
|
||||
const resolved = await adapter.resolveStream(upstreamUrl);
|
||||
upstreamUrl = resolved.masterUrl;
|
||||
}
|
||||
catch (err) {
|
||||
metrics.requests.errors++;
|
||||
metrics.upstream.notLive++;
|
||||
metrics.lastError = err.message;
|
||||
logger.warn(tag, `resolve failed: ${err.message} (${ms()})`);
|
||||
res.status(502).type('text/plain').send(`Resolve failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 3. Direct hop (master/variant/segment from a rewritten playlist) → SSRF gate.
|
||||
if (!proxy.isAllowedUpstream(upstreamUrl)) {
|
||||
logger.warn(tag, `400 blocked upstream: ${String(upstreamUrl).slice(0, 80)}`);
|
||||
res.status(400).type('text/plain').send('Bad request: upstream host not in the allowlist');
|
||||
return;
|
||||
}
|
||||
metrics.requests.total++;
|
||||
metrics.requests[proxy.classifyArtifact(upstreamUrl)]++;
|
||||
}
|
||||
const type = proxy.classifyArtifact(upstreamUrl);
|
||||
const { host, short } = label(upstreamUrl);
|
||||
metrics.active++;
|
||||
res.on('close', () => {
|
||||
metrics.active--;
|
||||
});
|
||||
let upstream;
|
||||
try {
|
||||
upstream = await fetch(upstreamUrl, { headers: proxy.upstreamHeaders(upstreamUrl) });
|
||||
}
|
||||
catch (err) {
|
||||
metrics.upstream.failed++;
|
||||
metrics.requests.errors++;
|
||||
metrics.lastError = err.message;
|
||||
logger.error(tag, `${type} ${host} ${short} upstream fetch failed: ${err.message} (${ms()})`);
|
||||
res.status(502).type('text/plain').send(`Upstream fetch failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
// Forward upstream errors verbatim. 404 = not transcoding right now; 403 = origin/referer gate.
|
||||
if (!upstream.ok) {
|
||||
metrics.requests.errors++;
|
||||
if (upstream.status === 404)
|
||||
metrics.upstream.notLive++;
|
||||
else if (upstream.status === 403)
|
||||
metrics.upstream.forbidden++;
|
||||
else
|
||||
metrics.upstream.failed++;
|
||||
const note = upstream.status === 404 ? ' (not live)' : upstream.status === 403 ? ' (gate)' : '';
|
||||
metrics.lastError = `HTTP ${upstream.status} ${host}/${short}`;
|
||||
logger.warn(tag, `${type} ${host} ${short} status=${upstream.status}${note} (${ms()})`);
|
||||
const detail = await upstream.text().catch(() => '');
|
||||
res
|
||||
.status(upstream.status)
|
||||
.type(upstream.headers.get('content-type') || 'text/plain')
|
||||
.send(detail || `Upstream HTTP ${upstream.status}`);
|
||||
return;
|
||||
}
|
||||
const contentType = upstream.headers.get('content-type') || '';
|
||||
// 4. Playlist → rewrite child URIs back through this source's proxy prefix
|
||||
// (and let the adapter learn each child host: dlhd dynamic-allow; dulo/common no-op).
|
||||
if (looksLikePlaylist(upstreamUrl, contentType)) {
|
||||
const rewritten = rewritePlaylist(await upstream.text(), upstreamUrl, PREFIX, proxy.onPlaylistChildHost);
|
||||
const bytes = Buffer.byteLength(rewritten);
|
||||
metrics.upstream.ok++;
|
||||
metrics.bytesStreamed += bytes;
|
||||
metrics.lastStreamAt = Date.now();
|
||||
logger.ok(tag, `${type} ${host} ${short} status=200 ${fmtBytes(bytes)} (${ms()})`);
|
||||
res.set('Cache-Control', 'no-store'); // playlists + tokens are short-lived
|
||||
res.type('application/vnd.apple.mpegurl').send(rewritten);
|
||||
return;
|
||||
}
|
||||
// 5. Segment (or anything else) → stream the bytes through, content-type per the adapter.
|
||||
res.set('Content-Type', proxy.relabelSegmentContentType(upstreamUrl, contentType, type));
|
||||
res.set('Cache-Control', 'no-store');
|
||||
if (!upstream.body) {
|
||||
metrics.upstream.ok++;
|
||||
logger.ok(tag, `${type} ${host} ${short} status=200 0B (${ms()})`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
let bytes = 0;
|
||||
const body = Readable.fromWeb(upstream.body);
|
||||
body.on('data', (chunk) => {
|
||||
bytes += chunk.length;
|
||||
});
|
||||
res.on('finish', () => {
|
||||
metrics.upstream.ok++;
|
||||
metrics.bytesStreamed += bytes;
|
||||
metrics.lastStreamAt = Date.now();
|
||||
logger.ok(tag, `${type} ${host} ${short} status=200 ${fmtBytes(bytes)} (${ms()})`);
|
||||
});
|
||||
body.on('error', (err) => {
|
||||
metrics.upstream.failed++;
|
||||
metrics.lastError = err.message;
|
||||
logger.error(tag, `${type} ${host} ${short} stream error: ${err.message}`);
|
||||
res.destroy(err);
|
||||
});
|
||||
body.pipe(res);
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=proxyHandler.js.map
|
||||
1
server/dist/sources/core/proxyHandler.js.map
vendored
Normal file
1
server/dist/sources/core/proxyHandler.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16
server/dist/sources/paths.js
vendored
Normal file
16
server/dist/sources/paths.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// This module lives at <root>/sources/paths.{ts,js} in BOTH dev (server/src) and prod (server/dist).
|
||||
// The bundled seed assets sit at server/seed-data/sources — i.e. two levels up from here, then
|
||||
// seed-data/sources. The Docker runtime stage copies server/seed-data → /app/seed-data alongside
|
||||
// /app/dist, so this same relative resolution holds in the container.
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
/** Directory holding the committed <id>.source.json baselines + <id>.snapshot.json fallbacks. */
|
||||
export const SEED_SOURCES_DIR = resolve(here, '..', '..', 'seed-data', 'sources');
|
||||
export function bundleFile(sourceId) {
|
||||
return resolve(SEED_SOURCES_DIR, `${sourceId}.source.json`);
|
||||
}
|
||||
export function snapshotFile(sourceId) {
|
||||
return resolve(SEED_SOURCES_DIR, `${sourceId}.snapshot.json`);
|
||||
}
|
||||
//# sourceMappingURL=paths.js.map
|
||||
1
server/dist/sources/paths.js.map
vendored
Normal file
1
server/dist/sources/paths.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/sources/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,qGAAqG;AACrG,+FAA+F;AAC/F,iGAAiG;AACjG,sEAAsE;AACtE,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErD,iGAAiG;AACjG,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;AAElF,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,OAAO,CAAC,gBAAgB,EAAE,GAAG,QAAQ,cAAc,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,OAAO,CAAC,gBAAgB,EAAE,GAAG,QAAQ,gBAAgB,CAAC,CAAC;AAChE,CAAC"}
|
||||
10
server/dist/sources/registry.js
vendored
Normal file
10
server/dist/sources/registry.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// The single enumeration of source adapters TVApp2 knows about. Ported from
|
||||
// ../d-combine/sources/registry.mjs. Adding a source (Phase 2: common, Phase 3: dlhd) = write a new
|
||||
// adapter under adapters/ and add it here; the boot init, sources router (manifest + proxy mounts),
|
||||
// and SPA all iterate this list, so nothing else needs to change.
|
||||
import duloAdapter from './adapters/dulo.js';
|
||||
export const SOURCES = [duloAdapter];
|
||||
export function getSource(id) {
|
||||
return SOURCES.find((s) => s.id === id);
|
||||
}
|
||||
//# sourceMappingURL=registry.js.map
|
||||
1
server/dist/sources/registry.js.map
vendored
Normal file
1
server/dist/sources/registry.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sources/registry.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,oGAAoG;AACpG,oGAAoG;AACpG,kEAAkE;AAElE,OAAO,WAAW,MAAM,oBAAoB,CAAC;AAG7C,MAAM,CAAC,MAAM,OAAO,GAAoB,CAAC,WAAW,CAAC,CAAC;AAEtD,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1C,CAAC"}
|
||||
137
server/dist/sources/seed.js
vendored
Normal file
137
server/dist/sources/seed.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
// Seed / init / sync / reset for the established (Default) source playlists.
|
||||
//
|
||||
// "Both: bundle + live sync" — the committed <id>.source.json bundle is the GUARANTEED baseline used
|
||||
// to initialize/reset MongoDB (offline-safe, reproducible); a live sync then refreshes channels and
|
||||
// the Playlist's sync metadata from upstream when reachable. Boot init (ensureSeeded + background
|
||||
// syncLive via bootInitSources) runs once per source in server/src/index.ts after the Mongo connect,
|
||||
// covering both Docker variants without extra orchestration.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { Playlist } from '../models/Playlist.js';
|
||||
import { SourceChannel } from '../models/SourceChannel.js';
|
||||
import { SOURCES, getSource } from './registry.js';
|
||||
import { buildSource } from './core/buildSource.js';
|
||||
import { bundleFile } from './paths.js';
|
||||
import { logger } from './core/logger.js';
|
||||
function readBundle(id) {
|
||||
const arr = JSON.parse(readFileSync(bundleFile(id), 'utf8'));
|
||||
if (!Array.isArray(arr))
|
||||
throw new Error(`bundle for "${id}" is not a JSON array`);
|
||||
return arr;
|
||||
}
|
||||
function groupCount(docs) {
|
||||
return new Set(docs.map((d) => d.groupKey)).size;
|
||||
}
|
||||
// Idempotent upsert by _id. Strips _id from $set (immutable; supplied by the filter on insert).
|
||||
async function upsertChannels(docs) {
|
||||
if (!docs.length)
|
||||
return;
|
||||
const ops = docs.map((d) => {
|
||||
const { _id, ...rest } = d;
|
||||
return { updateOne: { filter: { _id }, update: { $set: rest }, upsert: true } };
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await SourceChannel.bulkWrite(ops, { ordered: false });
|
||||
}
|
||||
async function upsertPlaylistRow(adapter, groups, opts) {
|
||||
const row = {
|
||||
id: adapter.id,
|
||||
source: adapter.id,
|
||||
name: `(Default) ${adapter.label}`,
|
||||
url: `source://${adapter.id}`,
|
||||
groups,
|
||||
lastSync: opts.lastSync,
|
||||
status: opts.status,
|
||||
auto: true,
|
||||
interval: 'Auto-updated',
|
||||
builtin: true,
|
||||
};
|
||||
await Playlist.updateOne({ id: adapter.id }, { $set: row }, { upsert: true });
|
||||
}
|
||||
export async function validateIntegrity(id) {
|
||||
const issues = [];
|
||||
const playlist = await Playlist.findOne({ id }).lean();
|
||||
const channelCount = await SourceChannel.countDocuments({ source: id });
|
||||
if (!playlist)
|
||||
issues.push('playlist row missing');
|
||||
if (channelCount === 0)
|
||||
issues.push('no channels');
|
||||
const sample = await SourceChannel.findOne({ source: id }).lean();
|
||||
if (sample) {
|
||||
for (const f of ['name', 'streamEntryUrl', 'groupKey']) {
|
||||
if (!sample[f])
|
||||
issues.push(`a channel is missing required field "${f}"`);
|
||||
}
|
||||
}
|
||||
return { id, playlistExists: !!playlist, channelCount, ok: issues.length === 0, issues };
|
||||
}
|
||||
/** Initialize a source from its committed bundle (idempotent upsert). */
|
||||
export async function initFromBundle(id) {
|
||||
const adapter = getSource(id);
|
||||
if (!adapter)
|
||||
throw new Error(`unknown source: ${id}`);
|
||||
const docs = readBundle(id);
|
||||
await upsertChannels(docs);
|
||||
await upsertPlaylistRow(adapter, groupCount(docs), { lastSync: 'Ships with TVApp2', status: 'good' });
|
||||
logger.ok('seed', `[${id}] initialized ${docs.length} channels from bundle`);
|
||||
return validateIntegrity(id);
|
||||
}
|
||||
/** Restore a source EXACTLY to its committed bundle (drops stale channels, then reinserts). */
|
||||
export async function resetFromBundle(id) {
|
||||
const adapter = getSource(id);
|
||||
if (!adapter)
|
||||
throw new Error(`unknown source: ${id}`);
|
||||
const docs = readBundle(id);
|
||||
await SourceChannel.deleteMany({ source: id });
|
||||
await SourceChannel.insertMany(docs, { ordered: false });
|
||||
await upsertPlaylistRow(adapter, groupCount(docs), { lastSync: 'Reset from bundle', status: 'good' });
|
||||
logger.ok('seed', `[${id}] reset ${docs.length} channels from bundle`);
|
||||
return validateIntegrity(id);
|
||||
}
|
||||
/** Live refresh: run the adapter's build pipeline and upsert; updates Playlist sync metadata. */
|
||||
export async function syncLive(id) {
|
||||
const adapter = getSource(id);
|
||||
if (!adapter)
|
||||
throw new Error(`unknown source: ${id}`);
|
||||
const result = await buildSource(adapter);
|
||||
await upsertChannels(result.docs);
|
||||
await upsertPlaylistRow(adapter, groupCount(result.docs), {
|
||||
lastSync: new Date().toISOString(),
|
||||
status: result.live ? 'good' : 'warn',
|
||||
});
|
||||
logger.ok('seed', `[${id}] live sync upserted ${result.count} channels (${result.live ? 'live' : 'snapshot'})`);
|
||||
const report = await validateIntegrity(id);
|
||||
return { report, live: result.live, count: result.count };
|
||||
}
|
||||
/** Boot guard: seed from bundle only if the (Default) playlist is missing or empty. */
|
||||
export async function ensureSeeded(id) {
|
||||
const report = await validateIntegrity(id);
|
||||
if (report.playlistExists && report.channelCount > 0) {
|
||||
logger.info('seed', `[${id}] already seeded (${report.channelCount} channels) — skipping init`);
|
||||
return report;
|
||||
}
|
||||
logger.info('seed', `[${id}] not seeded — initializing from bundle`);
|
||||
return initFromBundle(id);
|
||||
}
|
||||
/**
|
||||
* Run once at startup for every registered source: guarantee the bundle baseline synchronously, then
|
||||
* kick a non-blocking live sync (Both mode). A failed/slow live sync must NEVER block or crash boot —
|
||||
* the bundle baseline already satisfies init.
|
||||
*/
|
||||
export async function bootInitSources(opts = {}) {
|
||||
const liveSync = opts.liveSync ?? true;
|
||||
for (const adapter of SOURCES) {
|
||||
try {
|
||||
const report = await ensureSeeded(adapter.id);
|
||||
if (!report.ok) {
|
||||
logger.warn('seed', `[${adapter.id}] integrity issues after init: ${report.issues.join(', ')}`);
|
||||
}
|
||||
if (liveSync) {
|
||||
void syncLive(adapter.id).catch((err) => logger.warn('seed', `[${adapter.id}] live sync failed (keeping bundle baseline): ${err.message}`));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('seed', `[${adapter.id}] init failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=seed.js.map
|
||||
1
server/dist/sources/seed.js.map
vendored
Normal file
1
server/dist/sources/seed.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
46
server/dist/sources/translate.js
vendored
Normal file
46
server/dist/sources/translate.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
// Translation layer: project a canonical SourceChannel doc into the legacy UI Channel shape the Vue
|
||||
// screens consume. Fields with no source equivalent are returned as explicit null (never fabricated)
|
||||
// per the agreed schema-reconciliation decision; logoUrl + streamEntryUrl + isPlayable are added so
|
||||
// the SPA can render real logos and play through the proxy. The frontend derives the proxy path from
|
||||
// (source, streamEntryUrl); it is intentionally not stored.
|
||||
// Deterministic hue from a stable string → keeps a channel's fallback logo color stable across syncs.
|
||||
function hueFromString(s) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++)
|
||||
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
return h % 360;
|
||||
}
|
||||
function logoColorFor(id) {
|
||||
return `oklch(0.5 0.16 ${hueFromString(id)})`;
|
||||
}
|
||||
function initialsFor(name) {
|
||||
const ini = name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
return ini || '?';
|
||||
}
|
||||
export function toUiChannel(doc) {
|
||||
return {
|
||||
id: doc._id,
|
||||
tvg_name: doc.name,
|
||||
group: doc.groupLabel,
|
||||
channel: null,
|
||||
tvg_id: null,
|
||||
state: doc.isPlayable ? 'active' : 'disabled',
|
||||
epg: null,
|
||||
status: null,
|
||||
res: null,
|
||||
source: doc.source,
|
||||
url: doc.streamEntryUrl,
|
||||
logoColor: logoColorFor(doc._id),
|
||||
initials: initialsFor(doc.name),
|
||||
logoUrl: doc.logoUrl,
|
||||
streamEntryUrl: doc.streamEntryUrl,
|
||||
isPlayable: doc.isPlayable,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=translate.js.map
|
||||
1
server/dist/sources/translate.js.map
vendored
Normal file
1
server/dist/sources/translate.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/sources/translate.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,qGAAqG;AACrG,oGAAoG;AACpG,qGAAqG;AACrG,4DAA4D;AAuB5D,sGAAsG;AACtG,SAAS,aAAa,CAAC,CAAS;IAC9B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,GAAG,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,kBAAkB,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC;AAChD,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,GAAG,GAAG,IAAI;SACb,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,OAAO,CAAC;SACf,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAChB,IAAI,CAAC,EAAE,CAAC;SACR,WAAW,EAAE,CAAC;IACjB,OAAO,GAAG,IAAI,GAAG,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAqB;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,GAAG;QACX,QAAQ,EAAE,GAAG,CAAC,IAAI;QAClB,KAAK,EAAE,GAAG,CAAC,UAAU;QACrB,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;QAC7C,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,IAAI;QACZ,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,GAAG,EAAE,GAAG,CAAC,cAAc;QACvB,SAAS,EAAE,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;QAChC,QAAQ,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;AACJ,CAAC"}
|
||||
6
server/dist/sources/types.js
vendored
Normal file
6
server/dist/sources/types.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// The source-adapter contract, ported from d-combine (sources/<id>/adapter.mjs). One object per
|
||||
// source captures ONLY what differs between sources; the generic core (buildSource, proxyHandler,
|
||||
// playlist) consumes any adapter without per-source branching. Adding a source = one adapter +
|
||||
// one registry line.
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
server/dist/sources/types.js.map
vendored
Normal file
1
server/dist/sources/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sources/types.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,kGAAkG;AAClG,+FAA+F;AAC/F,qBAAqB"}
|
||||
1705
server/package-lock.json
generated
Normal file
1705
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
server/package.json
Normal file
23
server/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "tvapp2-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"seed": "tsx src/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"mongoose": "^8.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.10.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user