initial push from external dev branches

This commit is contained in:
iFlip721
2026-06-11 16:40:21 -04:00
parent 986e83632b
commit 245034a43a
182 changed files with 15465 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
Screenshots/TVApp2-Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View 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
View File

@@ -0,0 +1,5 @@
{
"mongoUri": "mongodb://tvapp:tvapp@mongo:27017/tvapp2?authSource=admin",
"port": 3000,
"logLevel": "info"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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};

File diff suppressed because one or more lines are too long

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
View 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};

File diff suppressed because one or more lines are too long

View 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};

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

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
View 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
View 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 &amp; 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
View File

@@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;

14
dist/types-node/vite.config.js vendored Normal file
View 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
View 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
View 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
View 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 &amp; 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>

Binary file not shown.

1575
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

19
server/dist/routes/programs.js vendored Normal file
View 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
View 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
View 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
View 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
View 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

View 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
View 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

File diff suppressed because one or more lines are too long

116
server/dist/sources/adapters/dulo.js vendored Normal file
View 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

View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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

File diff suppressed because one or more lines are too long

16
server/dist/sources/paths.js vendored Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

46
server/dist/sources/translate.js vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

23
server/package.json Normal file
View 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