diff --git a/Screenshots/Styles-Colors.png b/Screenshots/Styles-Colors.png new file mode 100644 index 00000000..296b0f5d Binary files /dev/null and b/Screenshots/Styles-Colors.png differ diff --git a/Screenshots/TVApp2-Logo.png b/Screenshots/TVApp2-Logo.png new file mode 100644 index 00000000..fff3ef0a Binary files /dev/null and b/Screenshots/TVApp2-Logo.png differ diff --git a/config/config.example.json b/config/config.example.json new file mode 100644 index 00000000..4f7e570e --- /dev/null +++ b/config/config.example.json @@ -0,0 +1,5 @@ +{ + "mongoUri": "mongodb://MONGO_ROOT_USER:MONGO_ROOT_PASS@mongo:27017/tvapp2?authSource=admin", + "port": 3000, + "logLevel": "info" +} diff --git a/config/config.json b/config/config.json new file mode 100644 index 00000000..16bbe531 --- /dev/null +++ b/config/config.json @@ -0,0 +1,5 @@ +{ + "mongoUri": "mongodb://tvapp:tvapp@mongo:27017/tvapp2?authSource=admin", + "port": 3000, + "logLevel": "info" +} diff --git a/dist/assets/ActiveStreamsScreen-Df8Wdk9n.js b/dist/assets/ActiveStreamsScreen-Df8Wdk9n.js new file mode 100644 index 00000000..85ede4e3 --- /dev/null +++ b/dist/assets/ActiveStreamsScreen-Df8Wdk9n.js @@ -0,0 +1 @@ +import{v as j,r,z as X,o as s,F as I,K as P,s as K,n as h,H as i,D as W,B as Y,t as n,N as e,O as U,A as Z,u as a,y as O,h as q,i as D,q as f,p as E,X as c,_ as g,e as w,S as Q,f as _,Z as ss,J as B,C as ts,c as es,g as ls,j as os,M as is}from"./index-CQPQcDLN.js";const as={style:{width:"100%"}},ns=["viewBox"],ds=["x2","y1","y2"],rs=["x2","y1","y2"],vs=["d"],us=["points"],ps=["cx","cy"],cs=["cx","cy"],N=760,C=90,x=6,ms=j({__name:"Sparkline",props:{series:{},target:{}},setup(H){const u=H,F=h(()=>Math.max(u.target*1.4,...u.series)),b=m=>x+m/(u.series.length-1)*(N-x*2),S=m=>C-x-(m-0)/(F.value-0)*(C-x*2),k=h(()=>u.series.map((m,l)=>`${b(l)},${S(m)}`).join(" ")),M=h(()=>`M${b(0)},${C} L ${k.value} L ${b(u.series.length-1)},${C} Z`),R=h(()=>S(u.target)),o=h(()=>b(u.series.length-1)),z=h(()=>S(u.series[u.series.length-1])),p=[.25,.5,.75];return(m,l)=>(i(),r("div",as,[(i(),r("svg",{viewBox:`0 0 ${N} ${C}`,preserveAspectRatio:"none",style:X({width:"100%",height:C+"px",display:"block"})},[l[0]||(l[0]=s("defs",null,[s("linearGradient",{id:"spark-grad",x1:"0",y1:"0",x2:"0",y2:"1"},[s("stop",{offset:"0%","stop-color":"oklch(0.82 0.13 220)","stop-opacity":"0.35"}),s("stop",{offset:"100%","stop-color":"oklch(0.82 0.13 220)","stop-opacity":"0"})])],-1)),(i(),r(I,null,P(p,V=>s("line",{key:V,x1:x,x2:N-x,y1:x+V*(C-x*2),y2:x+V*(C-x*2),stroke:"var(--hairline)","stroke-width":"1","stroke-dasharray":"2 4"},null,8,ds)),64)),s("line",{x1:x,x2:N-x,y1:R.value,y2:R.value,stroke:"oklch(0.82 0.13 220 / 0.5)","stroke-dasharray":"3 3","stroke-width":"1"},null,8,rs),s("path",{d:M.value,fill:"url(#spark-grad)"},null,8,vs),s("polyline",{fill:"none",stroke:"oklch(0.82 0.13 220)","stroke-width":"1.8","stroke-linejoin":"round","stroke-linecap":"round",points:k.value},null,8,us),s("circle",{cx:o.value,cy:z.value,r:"3.5",fill:"oklch(0.82 0.13 220)"},null,8,ps),s("circle",{cx:o.value,cy:z.value,r:"7",fill:"oklch(0.82 0.13 220 / 0.25)"},null,8,cs)],12,ns)),l[1]||(l[1]=K('
−60m−45m−30m−15mnow
',1))]))}}),ys={class:"col",style:{height:"100%","min-height":"0"}},fs={class:"stats"},gs={class:"card stat"},_s={class:"val"},xs={style:{color:"var(--text-3)","font-size":"16px","font-weight":"500"}},hs={class:"card stat"},bs={class:"val"},ks={class:"delta"},ws={class:"card stat"},zs={class:"val"},Ss={class:"card stat"},$s={class:"val"},Cs={class:"streams-grid"},Is={class:"streams-list"},Ms={class:"toolbar"},Es={class:"body"},Fs=["onClick"],Vs={style:{"min-width":"0"}},Bs={class:"nm"},Rs={style:{"white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis"}},Ls={key:0,class:"dot good pulse",style:{width:"6px",height:"6px"}},Ts={key:1,class:"dot warn",style:{width:"6px",height:"6px"}},Ns={key:2,class:"dot bad",style:{width:"6px",height:"6px"}},As={class:"meta"},Ds={class:"mono"},Ps={class:"mono"},Hs={class:"viewer"},Gs={class:"stream-detail"},Us={style:{padding:"var(--pad-card)",display:"flex",flexDirection:"column",gap:"16px"}},Os={class:"row",style:{gap:"14px"}},js={style:{flex:"1"}},Xs={class:"row",style:{gap:"10px"}},Js={style:{margin:"0","font-size":"17px","font-weight":"600"}},Ks={key:0,class:"live-pill"},Ws={class:"mono muted",style:{"font-size":"var(--fs-xs)","margin-top":"4px"}},Ys={style:{color:"var(--text-1)"}},Zs={class:"metric-grid"},qs={class:"metric"},Qs={class:"val"},st={class:"sub"},tt={class:"metric"},et={class:"val"},lt={key:0,style:{"font-size":"12px",color:"var(--text-2)","font-weight":"500"}},ot={class:"sub"},it={class:"metric"},at={class:"val"},nt={class:"sub"},dt={class:"metric"},rt={class:"val"},vt={class:"sub"},ut={class:"card",style:{background:"var(--bg-2)",padding:"14px"}},pt={class:"row",style:{"margin-bottom":"8px"}},ct={style:{display:"grid","grid-template-columns":"1fr 1fr",gap:"14px"}},mt={class:"card",style:{background:"var(--bg-2)",padding:"14px"}},yt={class:"kv-list"},ft={class:"v mono"},gt={class:"v mono"},_t={class:"v mono"},xt={class:"v mono"},ht={class:"v mono"},bt={class:"v mono"},kt={key:0,style:{color:"var(--bad)","margin-left":"8px"}},wt={class:"card",style:{background:"var(--bg-2)",padding:"14px"}},zt={class:"kv-list"},St={class:"v mono",style:{"font-size":"11px","word-break":"break-all"}},$t={class:"v mono"},Ct={class:"v mono"},It={key:1,style:{color:"var(--text-3)"}},Mt={class:"v"},Et={class:"v"},Ft={class:"card flush",style:{background:"var(--bg-2)"}},Vt={class:"card-hd",style:{padding:"12px 14px"}},Bt={key:0,class:"empty",style:{padding:"28px"}},Rt={key:1,class:"tbl"},Lt={class:"mono"},Tt={class:"muted"},Nt={class:"mono"},At={class:"muted"},Dt={class:"stream-view-hd"},Pt={style:{"min-width":"0",flex:"1"}},Ht={class:"row",style:{gap:"8px"}},Gt={style:{"font-weight":"600","font-size":"15px","white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis"}},Ut={key:0,class:"live-pill"},Ot={class:"mono muted",style:{"font-size":"var(--fs-xs)","margin-top":"3px"}},jt={class:"stream-view-body"},Xt={class:"player",style:{"border-radius":"12px"}},Jt={key:0,style:{position:"absolute",inset:"0",display:"grid","place-items":"center",color:"var(--text-2)","font-size":"13px"}},Kt={style:{"text-align":"center"}},Wt={style:{"margin-top":"16px"}},Yt={class:"label mono"},Zt={class:"play-btn"},qt={class:"controls"},Qt={class:"mono",style:{"font-size":"11px"}},se={class:"player-ctrl",title:"Fullscreen"},te={key:0,class:"card flush",style:{background:"var(--bg-2)"}},ee={key:0,style:{padding:"10px 12px","border-radius":"8px",background:"var(--accent-soft)",border:"1px solid oklch(0.82 0.13 220 / 0.4)"}},le={style:{"font-weight":"600","font-size":"14px","margin-top":"4px",color:"var(--accent-hi)"}},oe={class:"mono muted",style:{"font-size":"11px","margin-top":"4px"}},ie={key:1,style:{padding:"10px 12px","border-radius":"8px",background:"var(--bg-3)",border:"1px solid var(--hairline)"}},ae={style:{"font-weight":"600","font-size":"14px","margin-top":"4px",color:"var(--text-0)"}},ne={class:"mono muted",style:{"font-size":"11px","margin-top":"4px"}},de={class:"metric-grid",style:{"grid-template-columns":"repeat(4, 1fr)"}},re={class:"metric"},ve={class:"val",style:{"font-size":"17px"}},ue={class:"metric"},pe={class:"val",style:{"font-size":"17px"}},ce={class:"metric"},me={class:"val",style:{"font-size":"17px"}},ye={class:"metric"},fe={class:"val",style:{"font-size":"17px"}},ge={class:"card flush",style:{background:"var(--bg-2)"}},_e={class:"card-hd",style:{padding:"12px 14px"}},xe={style:{padding:"14px"}},he={class:"kv-list"},be={class:"v mono"},ke={class:"v mono"},we={class:"v mono"},ze={class:"v mono"},Se={class:"v mono"},$e={key:0,style:{color:"var(--bad)","margin-left":"8px"}},Ce={class:"v mono"},Ie={class:"v mono"},Me={class:"v mono",style:{"font-size":"11px","word-break":"break-all"}},Ee={class:"v mono"},Fe={key:1,style:{color:"var(--text-3)"}},Ve={class:"v"},Be={class:"row",style:{gap:"8px"}},Le=j({__name:"ActiveStreamsScreen",setup(H){const u=Z,F=B(u[0].id),b=B("all"),S=B(null),k=B(!0),M=B(!1),R=h(()=>u.filter(v=>b.value==="all"?!0:b.value==="issues"?v.status!=="good":b.value==="live"?v.status==="good":!0)),o=h(()=>u.find(v=>v.id===F.value)||u[0]),z=h(()=>({streams:u.filter(v=>v.status!=="bad").length,viewers:u.reduce((v,t)=>v+t.viewers,0),bandwidth:u.reduce((v,t)=>v+t.bandwidth,0),issues:u.filter(v=>v.status!=="good").length}));function p(v){return ts.find(t=>t.id===v.channelId)}const m=h(()=>is(o.value.uptimeMin+1,o.value.targetBitrate||5)),l=h(()=>u.find(v=>v.id===S.value));function V(){S.value=o.value.id,k.value=o.value.status!=="bad",M.value=!1}function A(){S.value=null}function G(v){v.key==="Escape"&&S.value&&A()}W(()=>window.addEventListener("keydown",G)),Y(()=>window.removeEventListener("keydown",G));function L(v){const t=Math.floor(v),d=Math.round((v-t)*60);return String(t).padStart(2,"0")+":"+String(d).padStart(2,"0")}function y(v){const t=es[v]||[],d=new Date().getHours()+new Date().getMinutes()/60,$=t.find(T=>d>=T.start&&dT.start>=($?$.end:d));return{live:$,next:J}}return(v,t)=>(i(),r("div",ys,[s("div",fs,[s("div",gs,[t[5]||(t[5]=s("div",{class:"lbl"},"Live now",-1)),s("div",_s,[n(e(z.value.streams),1),s("span",xs," / "+e(U(u).length),1)]),t[6]||(t[6]=s("div",{class:"delta"},[s("span",{class:"dot good pulse",style:{width:"6px",height:"6px"}}),n("relaying")],-1))]),s("div",hs,[t[8]||(t[8]=s("div",{class:"lbl"},"Viewers",-1)),s("div",bs,e(z.value.viewers),1),s("div",ks,[a(_,{name:"check",size:12}),t[7]||(t[7]=n("peak 412 today",-1))])]),s("div",ws,[t[10]||(t[10]=s("div",{class:"lbl"},"Egress",-1)),s("div",zs,[n(e((z.value.bandwidth/1e3).toFixed(2)),1),t[9]||(t[9]=s("span",{style:{"font-size":"14px",color:"var(--text-2)","font-weight":"500"}}," Gbps",-1))]),t[11]||(t[11]=s("div",{class:"delta"},"across 3 edge nodes",-1))]),s("div",Ss,[t[14]||(t[14]=s("div",{class:"lbl"},"Issues",-1)),s("div",$s,e(z.value.issues),1),s("div",{class:O(["delta",{bad:z.value.issues}])},[z.value.issues?(i(),r(I,{key:0},[a(_,{name:"warn",size:12}),t[12]||(t[12]=n("needs attention",-1))],64)):(i(),r(I,{key:1},[a(_,{name:"check",size:12}),t[13]||(t[13]=n("all healthy",-1))],64))],2)])]),s("div",Cs,[s("div",Is,[s("div",Ms,[a(os,{value:"",onChange:()=>{},placeholder:"Search streams",width:180}),t[15]||(t[15]=s("span",{class:"spacer"},null,-1)),a(q,{value:b.value,onChange:t[0]||(t[0]=d=>b.value=d),options:[{value:"all",label:"All"},{value:"live",label:"Live"},{value:"issues",label:"Issues"}]},null,8,["value"])]),s("div",Es,[(i(!0),r(I,null,P(R.value,d=>(i(),r("div",{key:d.id,class:O(["stream-item",{selected:F.value===d.id}]),onClick:$=>F.value=d.id},[a(D,{ch:p(d)},null,8,["ch"]),s("div",Vs,[s("div",Bs,[s("span",Rs,e(p(d).tvg_name),1),d.status==="good"?(i(),r("span",Ls)):d.status==="warn"?(i(),r("span",Ts)):(i(),r("span",Ns))]),s("div",As,[s("span",Ds,e(d.status==="bad"?"offline":d.resolution),1),t[16]||(t[16]=s("span",null,"·",-1)),s("span",Ps,e(d.status==="bad"?"—":d.bitrate.toFixed(1)+" Mbps"),1),t[17]||(t[17]=s("span",null,"·",-1)),s("span",null,e(d.uptime),1)])]),s("div",Hs,[s("b",null,e(d.viewers),1),t[18]||(t[18]=s("span",null,"viewers",-1))])],10,Fs))),128))])]),s("div",Gs,[s("div",Us,[s("div",Os,[a(D,{ch:p(o.value),size:"lg"},null,8,["ch"]),s("div",js,[s("div",Xs,[s("h2",Js,e(p(o.value).tvg_name),1),o.value.status!=="bad"?(i(),r("span",Ks,[...t[19]||(t[19]=[s("span",{class:"dot"},null,-1),n("LIVE",-1)])])):f("",!0),o.value.status==="bad"?(i(),E(g,{key:1,tone:"bad"},{default:c(()=>[a(_,{name:"warn",size:11}),t[20]||(t[20]=n("offline",-1))]),_:1})):f("",!0),o.value.status==="warn"?(i(),E(g,{key:2,tone:"warn"},{default:c(()=>[a(_,{name:"warn",size:11}),t[21]||(t[21]=n("degraded",-1))]),_:1})):f("",!0)]),s("div",Ws,[n(" #"+e(p(o.value).channel)+" · "+e(p(o.value).group)+" · stream-id ",1),s("span",Ys,e(o.value.id),1)])]),a(w,{variant:"ghost",size:"sm",icon:"refresh"},{default:c(()=>[...t[22]||(t[22]=[n("Restart",-1)])]),_:1}),a(w,{variant:o.value.status==="bad"?"primary":"ghost",size:"sm",icon:o.value.status==="bad"?"play":"stop"},{default:c(()=>[n(e(o.value.status==="bad"?"Start":"Stop"),1)]),_:1},8,["variant","icon"]),a(w,{variant:"primary",size:"sm",icon:"tv",onClick:V},{default:c(()=>[...t[23]||(t[23]=[n("View channel",-1)])]),_:1})]),s("div",Zs,[s("div",qs,[t[24]||(t[24]=s("div",{class:"lbl"},"Viewers",-1)),s("div",Qs,e(o.value.viewers),1),s("div",st,"peak "+e(o.value.peakViewers)+" · session",1)]),s("div",tt,[t[25]||(t[25]=s("div",{class:"lbl"},"Bitrate",-1)),s("div",et,[n(e(o.value.status==="bad"?"—":o.value.bitrate.toFixed(1)),1),o.value.status!=="bad"?(i(),r("span",lt," Mbps")):f("",!0)]),s("div",ot,"target "+e(o.value.targetBitrate.toFixed(1))+" Mbps",1)]),s("div",it,[t[26]||(t[26]=s("div",{class:"lbl"},"Uptime",-1)),s("div",at,e(o.value.uptime),1),s("div",nt,"since "+e(o.value.status==="bad"?"—":"started"),1)]),s("div",dt,[t[28]||(t[28]=s("div",{class:"lbl"},"Bandwidth",-1)),s("div",rt,[n(e(o.value.bandwidth),1),t[27]||(t[27]=s("span",{style:{"font-size":"12px",color:"var(--text-2)","font-weight":"500"}}," Mbps",-1))]),s("div",vt,"egress · "+e(o.value.viewers)+" client"+e(o.value.viewers===1?"":"s"),1)])]),s("div",ut,[s("div",pt,[t[29]||(t[29]=s("div",{style:{"font-size":"var(--fs-sm)","font-weight":"600"}},"Bitrate · last 60 min",-1)),t[30]||(t[30]=s("span",{class:"spacer"},null,-1)),a(g,{tone:"cyan"},{default:c(()=>[n("avg "+e((m.value.reduce((d,$)=>d+$,0)/m.value.length).toFixed(1))+" Mbps",1)]),_:1}),a(g,null,{default:c(()=>[n("min "+e(Math.min(...m.value).toFixed(1)),1)]),_:1}),a(g,null,{default:c(()=>[n("max "+e(Math.max(...m.value).toFixed(1)),1)]),_:1})]),a(ms,{series:m.value,target:o.value.targetBitrate},null,8,["series","target"])]),s("div",ct,[s("div",mt,[t[37]||(t[37]=s("div",{style:{"font-size":"var(--fs-sm)","font-weight":"600","margin-bottom":"12px"}},"Technical",-1)),s("div",yt,[t[31]||(t[31]=s("div",{class:"k"},"Video",-1)),s("div",ft,e(o.value.codec),1),t[32]||(t[32]=s("div",{class:"k"},"Audio",-1)),s("div",gt,e(o.value.audio),1),t[33]||(t[33]=s("div",{class:"k"},"Container",-1)),s("div",_t,e(o.value.container),1),t[34]||(t[34]=s("div",{class:"k"},"Resolution",-1)),s("div",xt,e(o.value.resolution)+" @ "+e(o.value.fps)+"fps",1),t[35]||(t[35]=s("div",{class:"k"},"Latency",-1)),s("div",ht,e(o.value.latency.toFixed(1))+" s",1),t[36]||(t[36]=s("div",{class:"k"},"Dropped",-1)),s("div",bt,[n(e(o.value.droppedFrames)+" frames · "+e((o.value.droppedRatio*100).toFixed(2))+"% ",1),o.value.droppedRatio>.1?(i(),r("span",kt,"● high")):f("",!0)])])]),s("div",wt,[t[47]||(t[47]=s("div",{style:{"font-size":"var(--fs-sm)","font-weight":"600","margin-bottom":"12px"}},"Source",-1)),s("div",zt,[t[40]||(t[40]=s("div",{class:"k"},"Upstream",-1)),s("div",St,e(o.value.sourceUrl),1),t[41]||(t[41]=s("div",{class:"k"},"Edge node",-1)),s("div",$t,e(o.value.sourceHost),1),t[42]||(t[42]=s("div",{class:"k"},"Protocol",-1)),t[43]||(t[43]=s("div",{class:"v mono"},"HLS · HTTPS",-1)),t[44]||(t[44]=s("div",{class:"k"},"TVG-ID",-1)),s("div",Ct,[p(o.value).tvg_id?(i(),r(I,{key:0},[n(e(p(o.value).tvg_id),1)],64)):(i(),r("span",It,"—"))]),t[45]||(t[45]=s("div",{class:"k"},"Source",-1)),s("div",Mt,[a(g,{tone:"cyan"},{default:c(()=>[n(e(p(o.value).source),1)]),_:1})]),t[46]||(t[46]=s("div",{class:"k"},"EPG",-1)),s("div",Et,[p(o.value).epg==="matched"?(i(),E(g,{key:0,tone:"good"},{default:c(()=>[a(_,{name:"check",size:11}),t[38]||(t[38]=n("matched",-1))]),_:1})):(i(),E(g,{key:1,tone:"warn"},{default:c(()=>[...t[39]||(t[39]=[n("unmatched",-1)])]),_:1}))])])])]),s("div",Ft,[s("div",Vt,[t[48]||(t[48]=s("h2",{style:{"font-size":"13px"}},"Connected sessions",-1)),a(g,{tone:"cyan"},{default:c(()=>[n(e(o.value.status==="bad"?0:o.value.viewers),1)]),_:1}),t[49]||(t[49]=s("span",{class:"spacer"},null,-1)),a(w,{variant:"ghost",size:"sm",icon:"more"})]),o.value.status==="bad"?(i(),r("div",Bt,[...t[50]||(t[50]=[s("div",{class:"muted"},"No viewers — stream is offline.",-1)])])):(i(),r("table",Rt,[t[51]||(t[51]=s("thead",null,[s("tr",null,[s("th",null,"IP"),s("th",null,"Region"),s("th",null,"Client"),s("th",null,"Bitrate"),s("th",{style:{width:"90px"}},"Joined"),s("th",{style:{width:"50px"}})])],-1)),s("tbody",null,[(i(!0),r(I,null,P(U(Q).slice(0,Math.min(o.value.viewers,6)),(d,$)=>(i(),r("tr",{key:$},[s("td",Lt,e(d.ip),1),s("td",Tt,e(d.region),1),s("td",null,e(d.client),1),s("td",Nt,e(d.bitrate),1),s("td",At,e(d.joined),1),s("td",null,[a(w,{variant:"ghost",size:"sm",icon:"x",title:"Disconnect"})])]))),128))])]))])])])]),l.value?(i(),r("div",{key:0,class:"stream-view-bg",onClick:A},[s("div",{class:"stream-view",onClick:t[4]||(t[4]=ss(()=>{},["stop"]))},[s("div",Dt,[a(D,{ch:p(l.value)},null,8,["ch"]),s("div",Pt,[s("div",Ht,[s("span",Gt,e(p(l.value).tvg_name),1),l.value.status!=="bad"?(i(),r("span",Ut,[...t[52]||(t[52]=[s("span",{class:"dot"},null,-1),n("LIVE",-1)])])):(i(),E(g,{key:1,tone:"bad"},{default:c(()=>[a(_,{name:"warn",size:11}),t[53]||(t[53]=n("offline",-1))]),_:1}))]),s("div",Ot," #"+e(p(l.value).channel)+" · "+e(p(l.value).group)+" · "+e(l.value.status==="bad"?"no signal":l.value.resolution+" · "+l.value.bitrate.toFixed(1)+" Mbps"),1)]),a(w,{variant:"ghost",size:"sm",icon:"x",onClick:A,title:"Close (Esc)"})]),s("div",jt,[s("div",Xt,[l.value.status==="bad"?(i(),r("div",Jt,[s("div",Kt,[a(_,{name:"warn",size:32}),t[55]||(t[55]=s("div",{style:{"margin-top":"12px","font-weight":"600",color:"var(--text-1)","font-size":"15px"}},"Stream offline",-1)),t[56]||(t[56]=s("div",{class:"mono",style:{"font-size":"11px","margin-top":"6px"}},"upstream returned HTTP 503",-1)),s("div",Wt,[a(w,{variant:"primary",size:"sm",icon:"refresh"},{default:c(()=>[...t[54]||(t[54]=[n("Retry source",-1)])]),_:1})])])])):(i(),r(I,{key:1},[t[59]||(t[59]=s("div",{class:"stripes"},null,-1)),s("div",Yt,e(l.value.resolution)+" · "+e(l.value.fps)+"fps · "+e(l.value.bitrate.toFixed(1))+" Mbps",1),k.value?f("",!0):(i(),r("div",{key:0,class:"play",onClick:t[1]||(t[1]=d=>k.value=!0)},[s("div",Zt,[a(_,{name:"play",size:28})])])),s("div",qt,[s("button",{class:"player-ctrl",onClick:t[2]||(t[2]=d=>k.value=!k.value)},[a(_,{name:k.value?"pause":"play",size:14},null,8,["name"])]),s("span",Qt,e(k.value?"01:42":"00:00"),1),t[57]||(t[57]=s("div",{class:"track"},null,-1)),s("button",{class:"player-ctrl",onClick:t[3]||(t[3]=d=>M.value=!M.value)},[a(_,{name:M.value?"x":"check",size:13},null,8,["name"])]),t[58]||(t[58]=s("span",{class:"mono",style:{"font-size":"11px"}},"LIVE",-1)),s("button",se,[a(_,{name:"grid",size:13})])])],64))]),l.value.status!=="bad"?(i(),r("div",te,[t[62]||(t[62]=s("div",{class:"card-hd",style:{padding:"12px 14px"}},[s("h2",{style:{"font-size":"13px"}},"From the guide"),s("span",{class:"spacer"}),s("span",{class:"muted",style:{"font-size":"var(--fs-xs)"}},"EPG-matched")],-1)),s("div",{style:X({padding:"14px",display:"grid",gridTemplateColumns:y(l.value.channelId).live&&y(l.value.channelId).next?"1fr 1fr":"1fr",gap:"12px"})},[y(l.value.channelId).live?(i(),r("div",ee,[t[60]||(t[60]=s("div",{class:"mono",style:{"font-size":"10px","letter-spacing":"0.08em",color:"var(--accent-hi)","font-weight":"600"}},"ON NOW",-1)),s("div",le,e(y(l.value.channelId).live.title),1),s("div",oe,e(L(y(l.value.channelId).live.start))+"–"+e(L(y(l.value.channelId).live.end))+" · "+e(y(l.value.channelId).live.cat),1)])):f("",!0),y(l.value.channelId).next?(i(),r("div",ie,[t[61]||(t[61]=s("div",{class:"mono",style:{"font-size":"10px","letter-spacing":"0.08em",color:"var(--text-2)","font-weight":"600"}},"UP NEXT",-1)),s("div",ae,e(y(l.value.channelId).next.title),1),s("div",ne,e(L(y(l.value.channelId).next.start))+"–"+e(L(y(l.value.channelId).next.end))+" · "+e(y(l.value.channelId).next.cat),1)])):f("",!0)],4)])):f("",!0),s("div",de,[s("div",re,[t[63]||(t[63]=s("div",{class:"lbl"},"Viewers",-1)),s("div",ve,e(l.value.viewers),1)]),s("div",ue,[t[64]||(t[64]=s("div",{class:"lbl"},"Bitrate",-1)),s("div",pe,e(l.value.status==="bad"?"—":l.value.bitrate.toFixed(1)+" Mbps"),1)]),s("div",ce,[t[65]||(t[65]=s("div",{class:"lbl"},"Latency",-1)),s("div",me,e(l.value.status==="bad"?"—":l.value.latency.toFixed(1)+"s"),1)]),s("div",ye,[t[66]||(t[66]=s("div",{class:"lbl"},"Uptime",-1)),s("div",fe,e(l.value.uptime),1)])]),s("div",ge,[s("div",_e,[t[67]||(t[67]=s("h2",{style:{"font-size":"13px"}},"Stream details",-1)),t[68]||(t[68]=s("span",{class:"spacer"},null,-1)),a(g,{tone:l.value.status==="bad"?"bad":"good"},{default:c(()=>[a(ls,{status:l.value.status,pulse:l.value.status!=="bad"},null,8,["status","pulse"]),n(" "+e(l.value.status==="bad"?"offline":l.value.status==="warn"?"degraded":"healthy"),1)]),_:1},8,["tone"])]),s("div",xe,[s("div",he,[t[69]||(t[69]=s("div",{class:"k"},"Video",-1)),s("div",be,e(l.value.codec),1),t[70]||(t[70]=s("div",{class:"k"},"Audio",-1)),s("div",ke,e(l.value.audio),1),t[71]||(t[71]=s("div",{class:"k"},"Container",-1)),s("div",we,e(l.value.container),1),t[72]||(t[72]=s("div",{class:"k"},"Resolution",-1)),s("div",ze,e(l.value.resolution)+" @ "+e(l.value.fps)+"fps",1),t[73]||(t[73]=s("div",{class:"k"},"Dropped",-1)),s("div",Se,[n(e(l.value.droppedFrames)+" frames · "+e((l.value.droppedRatio*100).toFixed(2))+"% ",1),l.value.droppedRatio>.1?(i(),r("span",$e,"● high")):f("",!0)]),t[74]||(t[74]=s("div",{class:"k"},"Bandwidth",-1)),s("div",Ce,e(l.value.bandwidth)+" Mbps egress",1),t[75]||(t[75]=s("div",{class:"k"},"Edge node",-1)),s("div",Ie,e(l.value.sourceHost),1),t[76]||(t[76]=s("div",{class:"k"},"Source URL",-1)),s("div",Me,e(l.value.sourceUrl),1),t[77]||(t[77]=s("div",{class:"k"},"TVG-ID",-1)),s("div",Ee,[p(l.value).tvg_id?(i(),r(I,{key:0},[n(e(p(l.value).tvg_id),1)],64)):(i(),r("span",Fe,"—"))]),t[78]||(t[78]=s("div",{class:"k"},"Source",-1)),s("div",Ve,[a(g,{tone:"cyan"},{default:c(()=>[n(e(p(l.value).source),1)]),_:1})])])])]),s("div",Be,[a(w,{variant:"ghost",icon:"refresh"},{default:c(()=>[...t[79]||(t[79]=[n("Restart stream",-1)])]),_:1}),a(w,{variant:"ghost",icon:"edit"},{default:c(()=>[...t[80]||(t[80]=[n("Edit channel",-1)])]),_:1}),t[82]||(t[82]=s("span",{class:"spacer"},null,-1)),l.value.status!=="bad"?(i(),E(w,{key:0,variant:"ghost",icon:"stop"},{default:c(()=>[...t[81]||(t[81]=[n("Stop",-1)])]),_:1})):f("",!0)])])])])):f("",!0)]))}});export{Le as default}; diff --git a/dist/assets/DashboardScreen-C8mAn1wl.js b/dist/assets/DashboardScreen-C8mAn1wl.js new file mode 100644 index 00000000..c0fcd1a2 --- /dev/null +++ b/dist/assets/DashboardScreen-C8mAn1wl.js @@ -0,0 +1 @@ +import{v as E,P as _,d as g,C as N,r as c,o as t,N as e,O as o,u as i,t as n,X as u,_ as y,e as v,F as h,K as p,a as V,Q as A,H as d,f as a,y as k,g as z,p as C,q as w}from"./index-CQPQcDLN.js";const T={class:"col",style:{gap:"18px"}},B={class:"stats"},G={class:"card stat"},H={class:"val"},I={class:"delta"},D={class:"card stat"},F={class:"val"},M={class:"delta"},O={class:"card stat"},R={class:"val"},U={class:"delta"},Y={class:"card stat"},q={class:"val"},K={class:"delta bad"},Q={style:{display:"grid","grid-template-columns":"1.4fr 1fr",gap:"18px"}},X={class:"col",style:{"min-width":"0"}},j={class:"card flush"},J={class:"card-hd"},W=["onClick"],Z={class:"src-name"},ss={class:"src-url"},ts={class:"stat-mini"},ls={class:"stat-mini"},is={class:"stat-mini",style:{"min-width":"110px"}},ns={style:{"font-size":"12px","font-weight":"500",color:"var(--text-1)"}},es={class:"card flush"},os={class:"card-hd"},as=["onClick"],ds={class:"src-name"},rs={class:"src-url"},us={class:"stat-mini"},cs={class:"stat-mini"},vs={class:"stat-mini",style:{"min-width":"110px"}},ms={style:{"font-size":"12px","font-weight":"500",color:"var(--text-1)"}},_s={class:"card flush"},gs={class:"ico-w"},ys={style:{flex:"1"}},fs=["innerHTML"],hs={class:"when"},ks=E({__name:"DashboardScreen",emits:["add"],setup(ps,{emit:x}){const b=x,S=A();function m(r){S.push(r)}const $=_.reduce((r,s)=>r+s.channels,0),L=g.reduce((r,s)=>r+s.programs,0),P=N.filter(r=>r.epg==="unmatched").length;return(r,s)=>(d(),c("div",T,[t("div",B,[t("div",G,[s[5]||(s[5]=t("div",{class:"lbl"},"Playlists",-1)),t("div",H,e(o(_).length),1),t("div",I,[i(a,{name:"check",size:12}),s[4]||(s[4]=n("all syncing",-1))])]),t("div",D,[s[7]||(s[7]=t("div",{class:"lbl"},"Channels",-1)),t("div",F,e(o($)),1),t("div",M,[i(a,{name:"plus",size:12}),s[6]||(s[6]=n("12 new this week",-1))])]),t("div",O,[s[8]||(s[8]=t("div",{class:"lbl"},"EPG sources",-1)),t("div",R,e(o(g).length),1),t("div",U,e(o(L).toLocaleString())+" programs",1)]),t("div",Y,[s[10]||(s[10]=t("div",{class:"lbl"},"Unmatched",-1)),t("div",q,e(o(P)),1),t("div",K,[i(a,{name:"warn",size:12}),s[9]||(s[9]=n("needs mapping",-1))])])]),t("div",Q,[t("div",X,[t("div",j,[t("div",J,[i(a,{name:"playlist",size:15}),s[13]||(s[13]=t("h2",null,"Playlists",-1)),i(y,{tone:"cyan"},{default:u(()=>[n(e(o(_).length),1)]),_:1}),s[14]||(s[14]=t("span",{class:"spacer"},null,-1)),i(v,{variant:"ghost",size:"sm",onClick:s[0]||(s[0]=l=>m("/playlists"))},{default:u(()=>[...s[11]||(s[11]=[n("View all",-1)])]),_:1}),i(v,{variant:"ghost",size:"sm",icon:"plus",onClick:s[1]||(s[1]=l=>b("add","playlist"))},{default:u(()=>[...s[12]||(s[12]=[n("Add playlist",-1)])]),_:1})]),(d(!0),c(h,null,p(o(_),l=>(d(),c("div",{key:l.id,class:"src-row",onClick:f=>m(`/playlists/${l.id}`)},[t("div",{class:k(["src-ico",{builtin:l.builtin}])},[i(a,{name:l.builtin?"tv":"playlist",size:18},null,8,["name"])],2),t("div",null,[t("div",Z,[n(e(l.name)+" ",1),i(z,{status:l.status,pulse:l.status==="good"},null,8,["status","pulse"]),l.builtin?(d(),C(y,{key:0,tone:"system"},{default:u(()=>[i(a,{name:"check",size:10}),s[15]||(s[15]=n("built-in",-1))]),_:1})):w("",!0)]),t("div",ss,e(l.url),1)]),t("div",ts,[t("b",null,e(l.channels),1),s[16]||(s[16]=n("channels",-1))]),t("div",ls,[t("b",null,e(l.groups),1),s[17]||(s[17]=n("groups",-1))]),t("div",is,[t("b",ns,e(l.lastSync),1),s[18]||(s[18]=n(" last sync ",-1))]),i(v,{variant:"ghost",size:"sm",icon:"chevron-r"})],8,W))),128))]),t("div",es,[t("div",os,[i(a,{name:"epg",size:15,style:{color:"var(--good)"}}),s[21]||(s[21]=t("h2",null,"EPG Sources",-1)),i(y,{tone:"good"},{default:u(()=>[n(e(o(g).length),1)]),_:1}),s[22]||(s[22]=t("span",{class:"spacer"},null,-1)),i(v,{variant:"ghost",size:"sm",onClick:s[2]||(s[2]=l=>m("/epg-sources"))},{default:u(()=>[...s[19]||(s[19]=[n("View all",-1)])]),_:1}),i(v,{variant:"ghost",size:"sm",icon:"plus",onClick:s[3]||(s[3]=l=>b("add","epg"))},{default:u(()=>[...s[20]||(s[20]=[n("Add EPG source",-1)])]),_:1})]),(d(!0),c(h,null,p(o(g),l=>(d(),c("div",{key:l.id,class:"src-row",onClick:f=>m(`/epg-sources/${l.id}`)},[t("div",{class:k(["src-ico",{builtin:l.builtin,"epg-builtin":l.builtin}]),style:{color:"var(--good)"}},[i(a,{name:l.builtin?"tv":"epg",size:18},null,8,["name"])],2),t("div",null,[t("div",ds,[n(e(l.name)+" ",1),i(z,{status:l.status,pulse:""},null,8,["status"]),l.builtin?(d(),C(y,{key:0,tone:"system"},{default:u(()=>[i(a,{name:"check",size:10}),s[23]||(s[23]=n("built-in",-1))]),_:1})):w("",!0)]),t("div",rs,e(l.url),1)]),t("div",us,[t("b",null,e(l.channels),1),s[24]||(s[24]=n("channels",-1))]),t("div",cs,[t("b",null,e(l.programs.toLocaleString()),1),s[25]||(s[25]=n("programs",-1))]),t("div",vs,[t("b",ms,e(l.lastSync),1),s[26]||(s[26]=n(" last sync ",-1))]),i(v,{variant:"ghost",size:"sm",icon:"chevron-r"})],8,as))),128))])]),t("div",_s,[s[27]||(s[27]=t("div",{class:"card-hd"},[t("h2",null,"Activity"),t("span",{class:"spacer"}),t("span",{class:"muted",style:{"font-size":"var(--fs-xs)"}},"Last 24h")],-1)),(d(!0),c(h,null,p(o(V),(l,f)=>(d(),c("div",{key:f,class:"act"},[t("div",gs,[i(a,{name:l.icon,size:14},null,8,["name"])]),t("div",ys,[t("div",{innerHTML:l.html},null,8,fs),t("div",hs,e(l.when)+" ago",1)])]))),128))])])]))}});export{ks as default}; diff --git a/dist/assets/EPGDetailScreen-CX4y1Ve9.js b/dist/assets/EPGDetailScreen-CX4y1Ve9.js new file mode 100644 index 00000000..7746a271 --- /dev/null +++ b/dist/assets/EPGDetailScreen-CX4y1Ve9.js @@ -0,0 +1 @@ +import{v as ee,D as V,B as W,r as v,o as e,y as H,u as a,f as _,N as n,g as te,p as x,X as p,_ as h,q as w,e as f,t as u,O as b,h as se,z as y,F as k,K as z,E as ne,i as L,Z as ae,R as oe,n as I,C as ie,J as U,H as o,c as K,j as le,d as j}from"./index-CQPQcDLN.js";import{_ as P}from"./Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js";const re={class:"col",style:{height:"100%"}},de={class:"card",style:{display:"flex","align-items":"center",gap:"16px"}},ue={style:{flex:"1"}},ve={class:"row",style:{gap:"10px"}},pe={style:{margin:"0","font-size":"18px","font-weight":"600"}},ce={class:"src-url",style:{"margin-top":"4px"}},ge={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"6px"}},me={class:"row",style:{gap:"18px"}},he={class:"card flush",style:{display:"flex","flex-direction":"column",flex:"1","min-height":"0"}},fe={class:"toolbar"},ye={class:"muted",style:{"font-size":"var(--fs-xs)"}},_e={class:"mono",style:{color:"var(--accent-hi)"}},xe={key:0,class:"epg",style:{flex:"1",overflow:"hidden"}},we={class:"epg-head"},ke={class:"epg-body"},be={class:"ch"},ze={style:{"min-width":"0"}},Se={class:"nm",style:{"white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis"}},Ce={class:"num mono"},Me=["onClick","title"],Ee={class:"t"},Te={class:"sub"},$e={key:1,style:{"overflow-y":"auto",flex:"1"}},Le={class:"row",style:{gap:"10px","margin-bottom":"10px"}},Pe={style:{"font-weight":"600"}},De={class:"mono muted",style:{"font-size":"var(--fs-xs)"}},Ne={style:{display:"grid","grid-template-columns":"repeat(auto-fill, minmax(220px, 1fr))",gap:"8px"}},Ae=["onClick"],Re={class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},Be={class:"stream-view-hd"},Ge={style:{"min-width":"0",flex:"1"}},Oe={class:"row",style:{gap:"8px"}},Fe={style:{"font-weight":"600","font-size":"15px","white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis"}},Ve={key:0,class:"live-pill"},We={class:"mono muted",style:{"font-size":"var(--fs-xs)","margin-top":"3px"}},He={class:"stream-view-body"},Ie={class:"player"},Ue={key:0,style:{position:"absolute",inset:"0",display:"grid","place-items":"center",color:"var(--text-2)","font-size":"13px"}},Ke={style:{"text-align":"center"}},je={class:"mono",style:{"font-size":"11px","margin-top":"6px"}},Xe={style:{"margin-top":"16px"}},qe={key:1,style:{position:"absolute",inset:"0",display:"grid","place-items":"center",color:"var(--text-2)","font-size":"13px"}},Je={style:{"text-align":"center"}},Qe={style:{"margin-top":"12px","font-weight":"600",color:"var(--text-1)","font-size":"15px"}},Ye={class:"mono",style:{"font-size":"11px","margin-top":"6px"}},Ze={style:{"margin-top":"16px"}},et={class:"label mono"},tt={class:"play"},st={class:"play-btn"},nt={class:"controls"},at={class:"mono",style:{"font-size":"11px"}},ot={class:"mono",style:{"font-size":"11px"}},it={style:{margin:"6px 0 8px","font-size":"22px","font-weight":"600","letter-spacing":"-0.015em"}},lt={class:"row",style:{gap:"6px"}},rt={key:0,class:"mono muted",style:{"font-size":"11px"}},dt={key:0,style:{"margin-top":"10px",height:"4px","border-radius":"999px",background:"var(--bg-3)",overflow:"hidden"}},ut={class:"card",style:{background:"var(--bg-2)",padding:"16px"}},vt={style:{"font-size":"var(--fs-sm)","line-height":"1.55",color:"var(--text-1)"}},pt={class:"card",style:{background:"var(--bg-2)",padding:"16px"}},ct={class:"kv-list"},gt={class:"v"},mt={class:"mono muted"},ht={class:"v"},ft={class:"v mono"},yt={class:"v mono"},_t={class:"v"},xt={class:"v mono"},wt={class:"v mono"},kt={key:1,style:{color:"var(--text-3)"}},bt={class:"v"},zt={class:"v"},St={class:"row",style:{gap:"8px"}},S=140,Et=ee({__name:"EPGDetailScreen",props:{id:{}},setup(X){const q=X,{tweaks:D}=oe(),g=I(()=>j.find(i=>i.id===q.id)||j[0]),N=ie.slice(0,12),r=U(0);function A(){const i=new Date;r.value=i.getHours()+i.getMinutes()/60}let C=null;V(()=>{A(),C=window.setInterval(A,6e4)}),W(()=>{C&&clearInterval(C)});const s=U(null);function R(i,t){s.value={channel:i,prog:t}}function M(){s.value=null}function c(i){const t=Math.floor(i),d=Math.round((i-t)*60);return String(t).padStart(2,"0")+":"+String(d).padStart(2,"0")}function E(i){const t=Math.floor(i),d=Math.round((i-t)*60);return t===0?`${d} min`:d===0?`${t} hr`:`${t} hr ${d} min`}function B(i){return i<1?Math.round(i*60)+" min":E(i)}const T=24*S,J=I(()=>"Today, "+new Date().toLocaleDateString(void 0,{weekday:"long",month:"short",day:"numeric"}));function G(i){i.key==="Escape"&&s.value&&M()}V(()=>window.addEventListener("keydown",G)),W(()=>window.removeEventListener("keydown",G));function m(i){return r.value>=i.start&&r.value=i.end?"past":"upcoming"}const Q={Live:"Live coverage with breaking updates, analysis and reports from correspondents on the ground.",News:"The latest national and international stories, plus business, sport, and a look at tomorrow's papers.",Documentary:"An in-depth feature on the world's most fascinating places, people, and events.",Lifestyle:"Fresh ideas for home, food, and travel — practical inspiration for everyday living.",Film:"A feature-length presentation. Cinematic storytelling with subtitles and audio description available.",Football:"Full match coverage with pre-match build-up, expert punditry, and post-match analysis.",Highlights:"The best moments and key plays condensed into a fast-paced roundup.",Comedy:"An evening of stand-up, sketches, and satire from familiar faces and rising stars.",Series:"The next instalment in our ongoing drama series. Contains scenes some viewers may find intense.",Music:"Back-to-back hits, exclusive sessions, and the latest releases from across the charts.",Kids:"Bright, friendly programming made just for younger viewers — learning through play.",Technology:"What's new in tech, gadgets, and software — reviews, deep-dives, and hands-on demos.",Discussion:"Panel conversation with guests dissecting the day's biggest stories.",Business:"Markets, deals, and the people moving them. Plus analysis from the trading floor.",Weather:"A full national outlook plus regional forecasts for the next 48 hours.","Game show":"Quick-fire rounds and big prizes — armchair contestants welcome.",Feature:"A standalone feature presentation tonight. Tune in for an unmissable story."};function O(i){return(K[i.id]||[]).filter(t=>t.end>=r.value-1).slice(0,6)}function F(i){return O(i).find(t=>r.value>=t.start&&r.value(o(),v("div",re,[e("div",de,[e("div",{class:H(["src-ico",{builtin:g.value.builtin,"epg-builtin":g.value.builtin}]),style:{width:"52px",height:"52px","border-radius":"12px",color:"var(--good)"}},[a(_,{name:g.value.builtin?"tv":"epg",size:22},null,8,["name"])],2),e("div",ue,[e("div",ve,[e("h2",pe,n(g.value.name),1),a(te,{status:g.value.status,pulse:""},null,8,["status"]),g.value.builtin?(o(),x(h,{key:0,tone:"system"},{default:p(()=>[a(_,{name:"check",size:10}),t[1]||(t[1]=u("built-in",-1))]),_:1})):w("",!0),a(h,{tone:"cyan"},{default:p(()=>[u(n(g.value.interval),1)]),_:1})]),e("div",ce,n(g.value.url),1),g.value.builtin?(o(),v("div",ge," Ships with TVApp2 · guide data is preconfigured and auto-updated with the app. ")):w("",!0)]),e("div",me,[a(P,{label:"Channels",value:g.value.channels},null,8,["value"]),a(P,{label:"Programs",value:g.value.programs.toLocaleString()},null,8,["value"]),a(P,{label:"Synced",value:g.value.lastSync,small:""},null,8,["value"])]),g.value.builtin?w("",!0):(o(),x(f,{key:0,variant:"ghost",icon:"refresh"},{default:p(()=>[...t[2]||(t[2]=[u("Sync now",-1)])]),_:1}))]),e("div",he,[e("div",fe,[a(f,{variant:"ghost",size:"sm",icon:"chevron-l"}),a(h,{tone:"cyan"},{default:p(()=>[a(_,{name:"epg",size:11}),u(" "+n(J.value),1)]),_:1}),a(f,{variant:"ghost",size:"sm",icon:"chevron-r"}),a(le,{value:"",onChange:()=>{},placeholder:"Filter channels",width:220}),t[4]||(t[4]=e("span",{class:"spacer"},null,-1)),e("span",ye,[t[3]||(t[3]=u(" Now: ",-1)),e("span",_e,n(String(Math.floor(r.value)).padStart(2,"0"))+":"+n(String(Math.floor(r.value%1*60)).padStart(2,"0")),1)]),a(se,{value:b(D).epgMode,onChange:()=>{},options:[{value:"timeline",label:"Timeline",icon:"grid"},{value:"list",label:"List",icon:"list"}]},null,8,["value"])]),b(D).epgMode==="timeline"?(o(),v("div",xe,[e("div",we,[t[5]||(t[5]=e("div",{class:"head-l"},"Channel",-1)),e("div",{class:"head-r",style:y({width:T+"px"})},[(o(!0),v(k,null,z(b(ne).slice(0,24),d=>(o(),v("div",{key:d,class:"epg-time",style:y({width:S+"px"})},n(String(d).padStart(2,"0"))+":00 ",5))),128))],4)]),e("div",ke,[e("div",{style:y({width:200+T+"px"})},[(o(!0),v(k,null,z(b(N),d=>(o(),v("div",{key:d.id,class:"epg-row"},[e("div",be,[a(L,{ch:d},null,8,["ch"]),e("div",ze,[e("div",Se,n(d.tvg_name),1),e("div",Ce,"#"+n(d.channel),1)])]),e("div",{class:"epg-progs",style:y({width:T+"px"})},[(o(!0),v(k,null,z(Y(d),(l,$)=>(o(),v("div",{key:$,class:H(["epg-prog",{live:r.value>=l.start&&r.valueR(d,l),title:`${l.title} · ${c(l.start)}–${c(l.end)}`},[e("div",Ee,n(l.title),1),e("div",Te,n(c(l.start))+"–"+n(c(l.end))+" · "+n(l.cat),1)],14,Me))),128)),e("div",{class:"now-line",style:y({left:r.value*S+"px"})},null,4)],4)]))),128))],4)])])):(o(),v("div",$e,[(o(!0),v(k,null,z(b(N),d=>(o(),v("div",{key:d.id,style:{"border-bottom":"1px solid var(--hairline)",padding:"14px var(--pad-card)"}},[e("div",Le,[a(L,{ch:d},null,8,["ch"]),e("div",null,[e("div",Pe,n(d.tvg_name),1),e("div",De,"#"+n(d.channel)+" · "+n(d.group),1)]),t[7]||(t[7]=e("span",{class:"spacer"},null,-1)),F(d)?(o(),x(h,{key:0,tone:"cyan"},{default:p(()=>[t[6]||(t[6]=e("span",{class:"dot good",style:{width:"6px",height:"6px"}},null,-1)),u("on now: "+n(F(d).title),1)]),_:2},1024)):w("",!0)]),e("div",Ne,[(o(!0),v(k,null,z(O(d),(l,$)=>(o(),v("div",{key:$,style:y({padding:"10px 12px",background:r.value>=l.start&&r.value=l.start&&r.valueR(d,l)},[e("div",{class:"mono",style:y({fontSize:"var(--fs-xs)",color:r.value>=l.start&&r.value=l.start&&r.value{},["stop"]))},[e("div",Be,[a(L,{ch:s.value.channel},null,8,["ch"]),e("div",Ge,[e("div",Oe,[e("span",Fe,n(s.value.channel.tvg_name),1),m(s.value.prog)==="live"?(o(),v("span",Ve,[...t[8]||(t[8]=[e("span",{class:"dot"},null,-1),u("LIVE",-1)])])):m(s.value.prog)==="upcoming"?(o(),x(h,{key:1,tone:"cyan"},{default:p(()=>[a(_,{name:"epg",size:11}),t[9]||(t[9]=u("upcoming",-1))]),_:1})):(o(),x(h,{key:2},{default:p(()=>[...t[10]||(t[10]=[u("aired",-1)])]),_:1}))]),e("div",We," #"+n(s.value.channel.channel)+" · "+n(s.value.channel.group)+" · "+n(s.value.channel.res),1)]),a(f,{variant:"ghost",size:"sm",icon:"x",onClick:M,title:"Close (Esc)"})]),e("div",He,[e("div",Ie,[m(s.value.prog)==="past"?(o(),v("div",Ue,[e("div",Ke,[a(_,{name:"epg",size:32}),t[12]||(t[12]=e("div",{style:{"margin-top":"12px","font-weight":"600",color:"var(--text-1)","font-size":"15px"}},"Programme has ended",-1)),e("div",je,"aired "+n(c(s.value.prog.start))+"–"+n(c(s.value.prog.end)),1),e("div",Xe,[a(f,{variant:"ghost",size:"sm",icon:"refresh"},{default:p(()=>[...t[11]||(t[11]=[u("Check on-demand",-1)])]),_:1})])])])):m(s.value.prog)==="upcoming"?(o(),v("div",qe,[e("div",Je,[a(_,{name:"epg",size:32}),e("div",Qe,"Starts at "+n(c(s.value.prog.start)),1),e("div",Ye,"in "+n(B(s.value.prog.start-r.value)),1),e("div",Ze,[a(f,{variant:"primary",size:"sm",icon:"add"},{default:p(()=>[...t[13]||(t[13]=[u("Set reminder",-1)])]),_:1})])])])):(o(),v(k,{key:2},[t[15]||(t[15]=e("div",{class:"stripes"},null,-1)),e("div",et,n(s.value.channel.res)+" · LIVE",1),e("div",tt,[e("div",st,[a(_,{name:"play",size:28})])]),e("div",nt,[a(_,{name:"pause",size:14}),e("span",at,n(c(r.value)),1),t[14]||(t[14]=e("div",{class:"track"},null,-1)),e("span",ot,n(c(s.value.prog.end)),1)])],64))]),e("div",null,[e("div",{class:"muted mono",style:y({fontSize:"10.5px",letterSpacing:"0.08em",textTransform:"uppercase",fontWeight:600,color:m(s.value.prog)==="live"?"var(--accent-hi)":"var(--text-2)"})},n(m(s.value.prog)==="live"?"ON NOW":m(s.value.prog)==="upcoming"?"UP NEXT":"EARLIER TODAY")+" · "+n(s.value.prog.cat),5),e("h2",it,n(s.value.prog.title),1),e("div",lt,[a(h,{tone:"cyan"},{default:p(()=>[a(_,{name:"epg",size:11}),u(n(c(s.value.prog.start))+"–"+n(c(s.value.prog.end)),1)]),_:1}),a(h,null,{default:p(()=>[u(n(E(s.value.prog.end-s.value.prog.start)),1)]),_:1}),a(h,null,{default:p(()=>[u(n(s.value.prog.cat),1)]),_:1}),t[16]||(t[16]=e("span",{class:"spacer"},null,-1)),m(s.value.prog)==="live"?(o(),v("span",rt,n(Math.round(Math.min(1,Math.max(0,(r.value-s.value.prog.start)/(s.value.prog.end-s.value.prog.start)))*100))+"% elapsed · "+n(B(s.value.prog.end-r.value))+" left ",1)):w("",!0)]),m(s.value.prog)==="live"?(o(),v("div",dt,[e("div",{style:y({height:"100%",width:Math.min(1,Math.max(0,(r.value-s.value.prog.start)/(s.value.prog.end-s.value.prog.start)))*100+"%",background:"var(--accent)",boxShadow:"0 0 12px var(--accent)"})},null,4)])):w("",!0)]),e("div",ut,[e("div",vt,n(Q[s.value.prog.cat]||"A scheduled programme on this channel."),1)]),e("div",pt,[t[28]||(t[28]=e("div",{style:{"font-size":"var(--fs-sm)","font-weight":"600","margin-bottom":"12px"}},"Programme details",-1)),e("div",ct,[t[19]||(t[19]=e("div",{class:"k"},"Channel",-1)),e("div",gt,[u(n(s.value.channel.tvg_name)+" ",1),e("span",mt,"· #"+n(s.value.channel.channel),1)]),t[20]||(t[20]=e("div",{class:"k"},"Group",-1)),e("div",ht,n(s.value.channel.group),1),t[21]||(t[21]=e("div",{class:"k"},"Time",-1)),e("div",ft,n(c(s.value.prog.start))+" – "+n(c(s.value.prog.end)),1),t[22]||(t[22]=e("div",{class:"k"},"Duration",-1)),e("div",yt,n(E(s.value.prog.end-s.value.prog.start)),1),t[23]||(t[23]=e("div",{class:"k"},"Category",-1)),e("div",_t,n(s.value.prog.cat),1),t[24]||(t[24]=e("div",{class:"k"},"Resolution",-1)),e("div",xt,n(s.value.channel.res),1),t[25]||(t[25]=e("div",{class:"k"},"TVG-ID",-1)),e("div",wt,[s.value.channel.tvg_id?(o(),v(k,{key:0},[u(n(s.value.channel.tvg_id),1)],64)):(o(),v("span",kt,"—"))]),t[26]||(t[26]=e("div",{class:"k"},"Source",-1)),e("div",bt,[a(h,{tone:"cyan"},{default:p(()=>[u(n(s.value.channel.source),1)]),_:1})]),t[27]||(t[27]=e("div",{class:"k"},"EPG match",-1)),e("div",zt,[s.value.channel.epg==="matched"?(o(),x(h,{key:0,tone:"good"},{default:p(()=>[a(_,{name:"check",size:11}),t[17]||(t[17]=u("matched",-1))]),_:1})):(o(),x(h,{key:1,tone:"warn"},{default:p(()=>[...t[18]||(t[18]=[u("unmatched",-1)])]),_:1}))])])]),e("div",St,[m(s.value.prog)==="upcoming"?(o(),x(f,{key:0,variant:"primary",icon:"add"},{default:p(()=>[...t[29]||(t[29]=[u("Set reminder",-1)])]),_:1})):w("",!0),m(s.value.prog)==="live"?(o(),x(f,{key:1,variant:"primary",icon:"play"},{default:p(()=>[...t[30]||(t[30]=[u("Watch live",-1)])]),_:1})):w("",!0),m(s.value.prog)==="past"?(o(),x(f,{key:2,variant:"ghost",icon:"refresh"},{default:p(()=>[...t[31]||(t[31]=[u("Check catch-up",-1)])]),_:1})):w("",!0),a(f,{variant:"ghost",icon:"tv"},{default:p(()=>[...t[32]||(t[32]=[u("Open channel",-1)])]),_:1}),a(f,{variant:"ghost",icon:"epg"},{default:p(()=>[...t[33]||(t[33]=[u("Channel guide",-1)])]),_:1}),t[34]||(t[34]=e("span",{class:"spacer"},null,-1)),a(f,{variant:"ghost",icon:"more"})])])])])):w("",!0)]))}});export{Et as default}; diff --git a/dist/assets/EPGSourcesScreen-Bh2qXoOm.js b/dist/assets/EPGSourcesScreen-Bh2qXoOm.js new file mode 100644 index 00000000..8773c3ca --- /dev/null +++ b/dist/assets/EPGSourcesScreen-Bh2qXoOm.js @@ -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}; diff --git a/dist/assets/HistoryMetricsScreen-DAcDrLQC.js b/dist/assets/HistoryMetricsScreen-DAcDrLQC.js new file mode 100644 index 00000000..ad29c08f --- /dev/null +++ b/dist/assets/HistoryMetricsScreen-DAcDrLQC.js @@ -0,0 +1 @@ +import{v as ls,C as k,J as M,D as as,r,o as s,u as v,h as K,X as z,e as ns,N as l,z as g,t as c,y as U,_ as V,F as y,K as S,s as is,j as rs,i as D,q,n as f,H as i,p as ds,f as X}from"./index-CQPQcDLN.js";const us={class:"col",style:{gap:"18px"}},vs={class:"card",style:{display:"flex","align-items":"center",gap:"12px",padding:"14px"}},cs={class:"stats"},fs={class:"card stat"},hs={class:"val"},ps={class:"delta"},gs={class:"card stat"},ms={class:"val"},_s={class:"delta"},ys={class:"card stat"},bs={class:"card stat"},xs={class:"delta"},ws={style:{display:"grid","grid-template-columns":"1.5fr 1fr",gap:"14px"}},ks={class:"card"},Ms={class:"row",style:{"margin-bottom":"10px"}},zs={class:"buf-bars"},Ss=["title"],Cs={class:"card"},$s={class:"col",style:{gap:"10px"}},As={style:{flex:"1","min-width":"0"}},Is={style:{"font-size":"var(--fs-sm)","font-weight":"500","white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis"}},Bs={class:"muted",style:{"font-size":"var(--fs-xs)"}},Ns={style:{"min-width":"50px","text-align":"right"}},Vs={class:"hm-grid"},Fs={class:"card flush hm-list"},Es={class:"toolbar"},Ls={class:"tbl"},Ps=["onClick"],Rs={class:"row",style:{gap:"8px"}},Ts={style:{"min-width":"0"}},Ds={style:{"font-weight":"500","white-space":"nowrap",overflow:"hidden","text-overflow":"ellipsis","max-width":"140px"}},qs={class:"mono muted",style:{"font-size":"10px"}},Os={class:"mono",style:{"font-size":"11px"}},Gs={class:"muted",style:{"font-size":"10px","margin-top":"2px"}},Hs={class:"muted mono"},Ws={class:"mono"},js={class:"row",style:{gap:"4px"}},Qs={class:"muted mono",style:{"font-size":"10px"}},Js=["data-health"],Ks={class:"card flush hm-detail"},Us={class:"card-hd",style:{padding:"14px var(--pad-card)"}},Xs={style:{"min-width":"0",flex:"1"}},Ys={style:{"font-weight":"600","font-size":"14px"}},Zs={class:"mono muted",style:{"font-size":"11px","margin-top":"2px"}},st=["data-health"],tt={style:{padding:"16px var(--pad-card) 0",display:"grid","grid-template-columns":"repeat(3, 1fr)",gap:"10px"}},et={class:"metric",style:{background:"var(--bg-2)"}},ot={class:"val",style:{fontSize:"17px"}},lt={class:"sub"},at={class:"metric",style:{background:"var(--bg-2)"}},nt={class:"val",style:{fontSize:"17px"}},it={class:"sub"},rt={class:"metric",style:{background:"var(--bg-2)"}},dt={class:"sub"},ut={style:{padding:"16px var(--pad-card)"}},vt={class:"kv-list",style:{"grid-template-columns":"120px 1fr"}},ct={class:"v mono"},ft={class:"v"},ht={class:"v"},pt={class:"v mono"},gt={class:"v mono"},mt={class:"v"},_t={class:"v mono"},yt={key:1,style:{color:"var(--text-3)"}},bt={class:"v"},xt={style:{padding:"0 var(--pad-card) 16px"}},wt={class:"row",style:{"margin-bottom":"8px"}},kt={class:"muted mono",style:{"font-size":"11px"}},Mt={class:"buf-timeline"},zt={key:0,class:"buf-timeline-empty"},St=["title"],Ct={key:0,class:"col",style:{gap:"6px","margin-top":"12px"}},$t={class:"mono",style:{width:"50px",color:"var(--text-1)"}},At={key:0,class:"muted mono",style:{"font-size":"11px"}},It={key:1,class:"empty"},Vt=ls({__name:"HistoryMetricsScreen",setup(Bt){const O=(()=>{const o=[{ip:"82.14.221.47",region:"GB · London",client:"VLC / Linux"},{ip:"192.81.45.12",region:"DE · Frankfurt",client:"Tivimate / Android TV"},{ip:"104.18.92.5",region:"NL · Amsterdam",client:"OTT Navigator / FireTV"},{ip:"176.58.103.9",region:"GB · Manchester",client:"Kodi 21"},{ip:"78.143.211.4",region:"FR · Paris",client:"IPTV Smarters / iOS"},{ip:"10.0.4.118",region:"Local · LAN",client:"ffmpeg / probe"},{ip:"165.225.18.4",region:"US · Ashburn",client:"Plex / NVIDIA Shield"},{ip:"203.0.113.91",region:"IE · Dublin",client:"VLC / macOS"},{ip:"51.15.88.142",region:"FR · Paris",client:"Jellyfin / FireTV"}];let t=99;const e=()=>(t=t*1664525+1013904223>>>0,t/4294967296),n=[];for(let u=0;u<28;u++){const p=k[u%k.length],m=o[Math.floor(e()*o.length)],B=Math.floor(e()*60*26),P=4+Math.floor(e()*180),_=Math.floor(e()*18),R=Math.floor(e()*350),N=3+e()*6,J=_*(200+Math.floor(e()*1800)),T=Math.max(0,Math.min(100,100-_*4-(J/100|0))),os=T<55?"bad":T<80?"warn":"good";n.push({id:"s-"+u,channelId:p.id,ip:m.ip,region:m.region,client:m.client,startedAgo:B,duration:P,buffers:_,rebuffMs:J,dropped:R,avgBitrate:+N.toFixed(1),resolution:p.res,codec:"H.264",score:T,health:os,ended:e()>.25})}return n.sort((u,p)=>u.startedAgo-p.startedAgo)})();function G(o){if(o<1)return"now";if(o<60)return o+"m ago";const t=Math.floor(o/60),e=o%60;return t<24?`${t}h ${e?e+"m":""}`.trim()+" ago":Math.floor(t/24)+"d ago"}function x(o){if(o<1e3)return o+"ms";const t=o/1e3;if(t<60)return t.toFixed(1)+"s";const e=Math.floor(t/60),n=Math.round(t%60);return`${e}m ${n}s`}function C(o){if(o<60)return o+"m";const t=Math.floor(o/60),e=o%60;return`${t}h ${e?e+"m":""}`.trim()}const H=M("24h"),w=M(""),$=M("all"),F=M(O[0].id),d=f(()=>O.filter(o=>{const t=k.find(e=>e.id===o.channelId);return!(w.value&&!t.tvg_name.toLowerCase().includes(w.value.toLowerCase())&&!o.ip.includes(w.value)||$.value!=="all"&&o.health!==$.value)})),A=f(()=>d.value.reduce((o,t)=>o+t.duration,0)),W=f(()=>d.value.reduce((o,t)=>o+t.buffers,0)),j=f(()=>d.value.reduce((o,t)=>o+t.rebuffMs,0)),E=f(()=>A.value?j.value/1e3/(A.value*60)*100:0),Y=f(()=>new Set(d.value.map(o=>o.ip)).size),Z=f(()=>new Set(d.value.map(o=>o.channelId)).size),Q=f(()=>d.value.length?Math.round(d.value.reduce((o,t)=>o+t.score,0)/d.value.length):0),I=f(()=>{const o=Array(24).fill(0);return d.value.forEach(t=>{const e=Math.min(23,Math.floor(t.startedAgo/60));o[23-e]+=t.buffers}),o}),ss=f(()=>{const o={};return d.value.forEach(t=>{const e=t.channelId;o[e]||(o[e]={ch:k.find(n=>n.id===e),sessions:0,buffers:0,scores:[]}),o[e].sessions++,o[e].buffers+=t.buffers,o[e].scores.push(t.score)}),Object.values(o).map(t=>({...t,avgScore:Math.round(t.scores.reduce((e,n)=>e+n,0)/t.scores.length)})).sort((t,e)=>t.avgScore-e.avgScore).slice(0,5)}),a=f(()=>d.value.find(o=>o.id===F.value)||d.value[0]);function h(o){return k.find(t=>t.id===o.channelId)}const L=M(!1);as(()=>requestAnimationFrame(()=>L.value=!0));function ts(o,t){const e=Math.max(1,...I.value),n=o/e,u=n*100,p=(.88-n*.33).toFixed(3),m=(.1+n*.07).toFixed(3),B=`oklch(${p} ${m} 220)`,P=1500,_=520,R=(P-_)/Math.max(1,I.value.length-1),N=t*R;return{height:L.value?u+"%":"0%",background:B,boxShadow:o>0&&L.value?`0 0 8px ${B}`:"none",transition:`height ${_}ms cubic-bezier(.2,.8,.2,1) ${N}ms, box-shadow 240ms ease ${N+_-100}ms`}}const b=f(()=>{if(!a.value)return[];let o=a.value.id.charCodeAt(2)+9;const t=()=>(o=o*1664525+1013904223>>>0,o/4294967296),e=[];for(let n=0;nn.at-u.at)});function es(o){return o==="good"?"var(--good)":o==="warn"?"var(--warn)":o==="bad"?"var(--bad)":"var(--text-0)"}return(o,t)=>(i(),r("div",us,[s("div",vs,[v(X,{name:"file",size:18}),t[4]||(t[4]=s("div",null,[s("div",{style:{"font-weight":"600","font-size":"15px"}},"Streaming history & metrics"),s("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}}," Past viewer sessions across all channels — identify channels with frequent rebuffering or playback issues. ")],-1)),t[5]||(t[5]=s("span",{class:"spacer"},null,-1)),v(K,{value:H.value,onChange:t[0]||(t[0]=e=>H.value=e),options:[{value:"1h",label:"1h"},{value:"24h",label:"24h"},{value:"7d",label:"7d"},{value:"30d",label:"30d"}]},null,8,["value"]),v(ns,{variant:"ghost",icon:"upload"},{default:z(()=>[...t[3]||(t[3]=[c("Export",-1)])]),_:1})]),s("div",cs,[s("div",fs,[t[6]||(t[6]=s("div",{class:"lbl"},"Sessions",-1)),s("div",hs,l(d.value.length),1),s("div",ps,l(Z.value)+" channels · "+l(Y.value)+" unique IPs",1)]),s("div",gs,[t[7]||(t[7]=s("div",{class:"lbl"},"Watch time",-1)),s("div",ms,l(C(A.value)),1),s("div",_s,"avg "+l(d.value.length?Math.round(A.value/d.value.length):0)+"m / session",1)]),s("div",ys,[t[9]||(t[9]=s("div",{class:"lbl"},"Rebuffer ratio",-1)),s("div",{class:"val",style:g(E.value>1.5?{color:"var(--warn)"}:void 0)},[c(l(E.value.toFixed(2)),1),t[8]||(t[8]=s("span",{style:{"font-size":"14px",color:"var(--text-2)","font-weight":"500"}},"%",-1))],4),s("div",{class:U(["delta",{bad:E.value>1.5}])},l(W.value)+" buffer events · "+l(x(j.value))+" total ",3)]),s("div",bs,[t[11]||(t[11]=s("div",{class:"lbl"},"QoE score",-1)),s("div",{class:"val",style:g({color:Q.value<70?"var(--warn)":"var(--good)"})},[c(l(Q.value),1),t[10]||(t[10]=s("span",{style:{"font-size":"14px",color:"var(--text-2)","font-weight":"500"}}," / 100",-1))],4),s("div",xs,l(d.value.filter(e=>e.health==="bad").length)+" problem sessions",1)])]),s("div",ws,[s("div",ks,[s("div",Ms,[t[12]||(t[12]=s("div",{style:{"font-weight":"600","font-size":"14px"}},"Buffer events · last 24h",-1)),t[13]||(t[13]=s("span",{class:"spacer"},null,-1)),v(V,{tone:"warn"},{default:z(()=>[c(l(W.value)+" total",1)]),_:1}),v(V,null,{default:z(()=>[c(l(Math.max(...I.value))+" peak / hour",1)]),_:1})]),s("div",null,[s("div",zs,[(i(!0),r(y,null,S(I.value,(e,n)=>(i(),r("div",{key:n,class:"buf-bar-wrap",title:`${23-n}h ago: ${e} buffer events`},[s("div",{class:"buf-bar",style:g(ts(e,n))},null,4)],8,Ss))),128))]),t[14]||(t[14]=is('
−24h−18h−12h−6hnow
',1))])]),s("div",Cs,[t[15]||(t[15]=s("div",{class:"row",style:{"margin-bottom":"10px"}},[s("div",{style:{"font-weight":"600","font-size":"14px"}},"Problem channels"),s("span",{class:"spacer"}),s("span",{class:"muted",style:{"font-size":"var(--fs-xs)"}},"by QoE score")],-1)),s("div",$s,[(i(!0),r(y,null,S(ss.value,e=>(i(),r("div",{key:e.ch.id,class:"row",style:{gap:"10px"}},[v(D,{ch:e.ch},null,8,["ch"]),s("div",As,[s("div",Is,l(e.ch.tvg_name),1),s("div",Bs,l(e.sessions)+" sessions · "+l(e.buffers)+" buffers",1)]),s("div",Ns,[s("span",{class:"mono",style:g({fontWeight:600,fontSize:"14px",color:e.avgScore<60?"var(--bad)":e.avgScore<80?"var(--warn)":"var(--good)"})},l(e.avgScore),5)])]))),128))])])]),s("div",Vs,[s("div",Fs,[s("div",Es,[v(rs,{value:w.value,onChange:t[1]||(t[1]=e=>w.value=e),placeholder:"Channel or IP",width:200},null,8,["value"]),t[16]||(t[16]=s("span",{class:"spacer"},null,-1)),v(K,{value:$.value,onChange:t[2]||(t[2]=e=>$.value=e),options:[{value:"all",label:"All"},{value:"good",label:"Good"},{value:"warn",label:"Warn"},{value:"bad",label:"Bad"}]},null,8,["value"])]),s("table",Ls,[t[19]||(t[19]=s("thead",null,[s("tr",null,[s("th",null,"Channel"),s("th",null,"IP / Region"),s("th",null,"Started"),s("th",null,"Duration"),s("th",null,"Buffers"),s("th",null,"QoE")])],-1)),s("tbody",null,[(i(!0),r(y,null,S(d.value,e=>(i(),r("tr",{key:e.id,class:U({selected:F.value===e.id}),onClick:n=>F.value=e.id},[s("td",null,[s("div",Rs,[v(D,{ch:h(e)},null,8,["ch"]),s("div",Ts,[s("div",Ds,l(h(e).tvg_name),1),s("div",qs,"#"+l(h(e).channel),1)])])]),s("td",null,[s("div",Os,l(e.ip),1),s("div",Gs,l(e.region),1)]),s("td",Hs,l(G(e.startedAgo)),1),s("td",Ws,[c(l(C(e.duration))+" ",1),e.ended?q("",!0):(i(),ds(V,{key:0,tone:"cyan",style:{"margin-left":"6px"}},{default:z(()=>[...t[17]||(t[17]=[c("live",-1)])]),_:1}))]),s("td",null,[s("div",js,[s("span",{class:"mono",style:g({fontWeight:600,color:e.buffers>6?"var(--warn)":"var(--text-1)"})},l(e.buffers),5),s("span",Qs,"· "+l(x(e.rebuffMs)),1)])]),s("td",null,[s("div",{class:"qoe-pill","data-health":e.health},[t[18]||(t[18]=s("span",{class:"dot"},null,-1)),c(l(e.score),1)],8,Js)])],10,Ps))),128))])])]),s("div",Ks,[a.value?(i(),r(y,{key:0},[s("div",Us,[v(D,{ch:h(a.value)},null,8,["ch"]),s("div",Xs,[s("div",Ys,l(h(a.value).tvg_name),1),s("div",Zs,"#"+l(h(a.value).channel)+" · session "+l(a.value.id),1)]),s("div",{class:"qoe-pill","data-health":a.value.health,style:{"font-size":"13px",padding:"4px 12px"}},[t[20]||(t[20]=s("span",{class:"dot"},null,-1)),c("QoE "+l(a.value.score),1)],8,st)]),s("div",tt,[s("div",et,[t[21]||(t[21]=s("div",{class:"lbl"},"Duration",-1)),s("div",ot,l(C(a.value.duration)),1),s("div",lt,"started "+l(G(a.value.startedAgo)),1)]),s("div",at,[t[22]||(t[22]=s("div",{class:"lbl"},"Avg bitrate",-1)),s("div",nt,l(a.value.avgBitrate)+" Mbps",1),s("div",it,l(a.value.resolution)+" · "+l(a.value.codec),1)]),s("div",rt,[t[23]||(t[23]=s("div",{class:"lbl"},"Rebuffer",-1)),s("div",{class:"val",style:g({color:es(a.value.buffers>6?"warn":"good"),fontSize:"17px"})},l(x(a.value.rebuffMs)),5),s("div",dt,l(a.value.buffers)+" events · "+l(a.value.dropped)+" drops",1)])]),s("div",ut,[s("div",vt,[t[24]||(t[24]=s("div",{class:"k"},"Client IP",-1)),s("div",ct,l(a.value.ip),1),t[25]||(t[25]=s("div",{class:"k"},"Region",-1)),s("div",ft,l(a.value.region),1),t[26]||(t[26]=s("div",{class:"k"},"Player",-1)),s("div",ht,l(a.value.client),1),t[27]||(t[27]=s("div",{class:"k"},"Resolution",-1)),s("div",pt,l(a.value.resolution)+" · "+l(a.value.codec),1),t[28]||(t[28]=s("div",{class:"k"},"Channel #",-1)),s("div",gt,"#"+l(h(a.value).channel),1),t[29]||(t[29]=s("div",{class:"k"},"Group",-1)),s("div",mt,l(h(a.value).group),1),t[30]||(t[30]=s("div",{class:"k"},"TVG-ID",-1)),s("div",_t,[h(a.value).tvg_id?(i(),r(y,{key:0},[c(l(h(a.value).tvg_id),1)],64)):(i(),r("span",yt,"—"))]),t[31]||(t[31]=s("div",{class:"k"},"Source",-1)),s("div",bt,[v(V,{tone:"cyan"},{default:z(()=>[c(l(h(a.value).source),1)]),_:1})])])]),s("div",xt,[s("div",wt,[t[32]||(t[32]=s("div",{style:{"font-size":"var(--fs-sm)","font-weight":"600"}},"Buffering timeline",-1)),t[33]||(t[33]=s("span",{class:"spacer"},null,-1)),s("span",kt,"0 → "+l(C(a.value.duration)),1)]),s("div",Mt,[b.value.length===0?(i(),r("div",zt,[v(X,{name:"check",size:12}),t[34]||(t[34]=c(" No buffer events ",-1))])):(i(!0),r(y,{key:1},S(b.value,(e,n)=>(i(),r("div",{key:n,class:"buf-event",style:g({left:e.at/a.value.duration*100+"%",width:Math.max(2,e.dur/6e4/a.value.duration*100)+"%"}),title:`${x(e.dur)} stall at ${e.at}m — ${e.cause}`},null,12,St))),128))]),b.value.length>0?(i(),r("div",Ct,[(i(!0),r(y,null,S(b.value.slice(0,6),(e,n)=>(i(),r("div",{key:n,class:"row",style:{"font-size":"var(--fs-xs)",color:"var(--text-2)"}},[s("span",$t,"+"+l(e.at)+"m",1),s("span",{class:"mono",style:g({width:"70px",color:e.dur>1e3?"var(--warn)":"var(--text-1)"})},l(x(e.dur)),5),s("span",null,l(e.cause),1)]))),128)),b.value.length>6?(i(),r("span",At,"+ "+l(b.value.length-6)+" more",1)):q("",!0)])):q("",!0)])],64)):(i(),r("div",It,[...t[35]||(t[35]=[s("h3",null,"No session",-1)])]))])])]))}});export{Vt as default}; diff --git a/dist/assets/ImportScreen-D7vLRk6-.js b/dist/assets/ImportScreen-D7vLRk6-.js new file mode 100644 index 00000000..7ed4ec37 --- /dev/null +++ b/dist/assets/ImportScreen-D7vLRk6-.js @@ -0,0 +1 @@ +import{v as $,r as f,o as l,y as k,u as t,t as s,Z as h,N as d,X as i,_ as r,p as z,e as c,z as D,s as U,q as b,Y as V,U as S,J as y,H as p,Q as B,f as u}from"./index-CQPQcDLN.js";const E={class:"col",style:{"max-width":"760px"}},N={class:"segmented",style:{"align-self":"flex-start"}},I={class:"card"},P={class:"segmented",style:{"margin-bottom":"14px"}},R={class:"active"},T={class:"icon-circle"},G={class:"row",style:{gap:"6px",color:"var(--text-3)","font-size":"var(--fs-xs)"}},L={key:1,class:"card",style:{background:"var(--bg-2)"}},X={class:"row",style:{"margin-bottom":"12px"}},j={style:{flex:"1"}},q={style:{"font-weight":"600"}},F={class:"muted",style:{"font-size":"var(--fs-xs)"}},H={style:{height:"4px","border-radius":"999px",background:"var(--bg-3)",overflow:"hidden"}},J={key:0,style:{"margin-top":"16px",display:"flex","flex-direction":"column",gap:"12px"}},O={class:"form-grid-2"},Q={class:"form-row"},Y={class:"input"},Z=["value"],A={class:"row",style:{"margin-top":"4px"}},K={class:"row",style:{"justify-content":"flex-end","margin-top":"6px"}},W={key:0,class:"card"},ee={class:"row"},le={class:"input",style:{flex:"1"}},te=["placeholder"],ae=$({__name:"ImportScreen",setup(se){const w=B(),a=y("playlist"),g=y(!1),v=y(null),n=y(0),_=y("");function x(m){v.value={name:m,size:"1.2 MB"},n.value=0;let e=0;const o=setInterval(()=>{e+=7+Math.random()*10,e>=100&&(e=100,clearInterval(o)),n.value=Math.round(e)},180)}function M(m){var o;m.preventDefault(),g.value=!1;const e=(o=m.dataTransfer)==null?void 0:o.files[0];e&&x(e.name)}function C(){w.push(a.value==="playlist"?"/playlists":"/epg-sources")}return(m,e)=>(p(),f("div",E,[l("div",N,[l("button",{class:k(a.value==="playlist"?"active":""),onClick:e[0]||(e[0]=o=>a.value="playlist")},[t(u,{name:"playlist",size:13}),e[9]||(e[9]=s("M3U Playlist ",-1))],2),l("button",{class:k(a.value==="epg"?"active":""),onClick:e[1]||(e[1]=o=>a.value="epg")},[t(u,{name:"epg",size:13}),e[10]||(e[10]=s("EPG / XMLTV ",-1))],2)]),l("div",I,[e[22]||(e[22]=l("div",{class:"field-lbl"},"Source",-1)),l("div",P,[l("button",R,[t(u,{name:"upload",size:13}),e[11]||(e[11]=s("Upload file",-1))]),l("button",null,[t(u,{name:"link",size:13}),e[12]||(e[12]=s("Remote URL",-1))])]),v.value?(p(),f("div",L,[l("div",X,[t(u,{name:"file",size:18}),l("div",j,[l("div",q,d(v.value.name),1),l("div",F,d(v.value.size)+" · "+d(n.value<100?"parsing…":"ready"),1)]),n.value<100?(p(),z(r,{key:0,tone:"cyan"},{default:i(()=>[s(d(n.value)+"%",1)]),_:1})):(p(),z(r,{key:1,tone:"good"},{default:i(()=>[t(u,{name:"check",size:11}),e[18]||(e[18]=s("parsed",-1))]),_:1})),t(c,{variant:"ghost",size:"sm",icon:"x",onClick:e[5]||(e[5]=o=>{v.value=null,n.value=0})})]),l("div",H,[l("div",{style:D({height:"100%",width:n.value+"%",background:"var(--accent)",boxShadow:"0 0 12px var(--accent)",transition:"width .15s"})},null,4)]),n.value===100?(p(),f("div",J,[l("div",O,[l("div",Q,[e[19]||(e[19]=l("div",{class:"field-lbl"},"Source name",-1)),l("div",Y,[l("input",{value:a.value==="playlist"?"My Playlist":"My EPG Guide"},null,8,Z)])]),e[20]||(e[20]=U('
Refresh interval
',1))]),l("div",A,[t(r,{tone:"good"},{default:i(()=>[s(d(a.value==="playlist"?"142 channels detected":"8,420 programs detected"),1)]),_:1}),t(r,null,{default:i(()=>[s(d(a.value==="playlist"?"8 groups":"124 channels"),1)]),_:1})]),l("div",K,[t(c,{variant:"ghost",onClick:e[6]||(e[6]=o=>{v.value=null,n.value=0})},{default:i(()=>[...e[21]||(e[21]=[s("Cancel",-1)])]),_:1}),t(c,{variant:"primary",icon:"check",onClick:C},{default:i(()=>[s(" Import "+d(a.value==="playlist"?"playlist":"EPG"),1)]),_:1})])])):b("",!0)])):(p(),f("div",{key:0,class:k(["dropzone",{over:g.value}]),onDragover:e[2]||(e[2]=h(o=>g.value=!0,["prevent"])),onDragleave:e[3]||(e[3]=o=>g.value=!1),onDrop:M,onClick:e[4]||(e[4]=o=>x(a.value==="playlist"?"playlist.m3u":"guide.xml.gz"))},[l("div",T,[t(u,{name:"upload",size:22})]),l("div",null,[l("h3",null,"Drop "+d(a.value==="playlist"?"an M3U/M3U8":"an XMLTV")+" file here",1),e[13]||(e[13]=l("p",null,"or click to browse — up to 50 MB",-1))]),l("div",G,[t(r,null,{default:i(()=>[...e[14]||(e[14]=[s(".m3u",-1)])]),_:1}),t(r,null,{default:i(()=>[...e[15]||(e[15]=[s(".m3u8",-1)])]),_:1}),t(r,null,{default:i(()=>[...e[16]||(e[16]=[s(".xml",-1)])]),_:1}),t(r,null,{default:i(()=>[...e[17]||(e[17]=[s(".xml.gz",-1)])]),_:1})])],34))]),v.value?b("",!0):(p(),f("div",W,[e[24]||(e[24]=l("div",{class:"field-lbl"},"Or paste a URL",-1)),l("div",ee,[l("div",le,[t(u,{name:"link",size:14}),V(l("input",{"onUpdate:modelValue":e[7]||(e[7]=o=>_.value=o),placeholder:a.value==="playlist"?"https://example.com/playlist.m3u":"https://example.com/guide.xml.gz"},null,8,te),[[S,_.value]])]),t(c,{variant:"primary",icon:"import",onClick:e[8]||(e[8]=o=>x(a.value==="playlist"?"remote.m3u":"remote.xml.gz"))},{default:i(()=>[...e[23]||(e[23]=[s("Fetch",-1)])]),_:1})])]))]))}});export{ae as default}; diff --git a/dist/assets/MappingScreen-BdiMBcth.js b/dist/assets/MappingScreen-BdiMBcth.js new file mode 100644 index 00000000..80620394 --- /dev/null +++ b/dist/assets/MappingScreen-BdiMBcth.js @@ -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}; diff --git a/dist/assets/PlaylistDetailScreen-F92VSAQ7.js b/dist/assets/PlaylistDetailScreen-F92VSAQ7.js new file mode 100644 index 00000000..e9d5d023 --- /dev/null +++ b/dist/assets/PlaylistDetailScreen-F92VSAQ7.js @@ -0,0 +1 @@ +import{v as Z,r as u,y as K,H as i,o as e,u as l,N as o,e as k,X as p,_ as x,F as V,K as O,q as D,t as d,h as ge,p as T,Y as F,T as ae,O as P,G as fe,U as oe,J as b,n as h,f,k as Me,z as te,W as Ee,V as ce,g as le,j as Ue,Z as H,b as Le,w as Ve,i as me}from"./index-CQPQcDLN.js";import{u as ye,m as Ge,p as Ie,a as Re}from"./useSettings-CPUgOpin.js";import{_ as W}from"./Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js";const Te=["aria-checked"],ne=Z({__name:"Checkbox",props:{on:{type:Boolean}},emits:["change"],setup(S,{emit:B}){const _=S,E=B;function m(g){g.stopPropagation(),E("change",!_.on)}return(g,U)=>(i(),u("div",{class:K(["cbx",{on:_.on}]),role:"checkbox","aria-checked":!!_.on,onClick:m},null,10,Te))}}),Oe={class:"drawer-wrap"},Be={class:"glass drawer-panel"},Ne={class:"drawer-hd"},je={class:"src-ico",style:{width:"44px",height:"44px","border-radius":"10px"}},Ke={style:{flex:"1"}},He={style:{"font-weight":"600","font-size":"15px"}},Fe={class:"drawer-body"},Ye={style:{border:"1px solid var(--hairline)","border-radius":"10px",padding:"10px 12px",background:"var(--bg-2)","max-height":"168px",overflow:"auto"}},qe={class:"row",style:{gap:"8px","margin-bottom":"8px"}},Je={class:"mono muted",style:{"font-size":"var(--fs-xs)","min-width":"32px"}},We={style:{"font-weight":"500"}},Xe={class:"muted",style:{"font-size":"var(--fs-xs)"}},Ze={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","padding-top":"6px"}},Qe={class:"form-row"},es={class:"field-lbl"},ss={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","margin-left":"6px"}},ts={class:"row",style:{gap:"10px"}},ls={class:"form-row"},ns={class:"field-lbl"},as={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","margin-left":"6px"}},os={class:"select"},is={value:""},ds=["value"],us={class:"form-row"},rs={class:"field-lbl"},vs={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","margin-left":"6px"}},ps={class:"input"},cs=["placeholder"],ms={class:"row",style:{"margin-top":"6px"}},gs=Z({__name:"ChannelBulkDrawer",props:{channels:{}},emits:["close","apply"],setup(S,{emit:B}){const _=S,E=B,m=b(""),g=b(""),U=b(""),G=h(()=>new Set(_.channels.map(r=>r.state)).size>1),I=h(()=>new Set(_.channels.map(r=>r.group)).size>1),c=h(()=>new Set(_.channels.map(r=>r.source)).size>1),A=h(()=>{var v;return G.value?"":((v=_.channels[0])==null?void 0:v.state)??""}),M=h(()=>{var v;return I.value?"":((v=_.channels[0])==null?void 0:v.group)??""}),$=h(()=>{var v;return c.value?"":((v=_.channels[0])==null?void 0:v.source)??""});function y(v){m.value=v}function n(){const v={};m.value&&m.value!==A.value&&(v.state=m.value),g.value&&g.value!==M.value&&(v.group=g.value),U.value&&U.value!==$.value&&(v.source=U.value),E("apply",v),E("close")}return(v,r)=>(i(),u("div",Oe,[e("div",{class:"glass-bg drawer-backdrop",onClick:r[0]||(r[0]=z=>E("close"))}),e("div",Be,[e("div",Ne,[e("div",je,[l(f,{name:"edit",size:20})]),e("div",Ke,[e("div",He,"Edit "+o(S.channels.length)+" channels",1),r[5]||(r[5]=e("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}}," Apply changes to all selected channels ",-1))]),l(k,{variant:"ghost",size:"sm",icon:"x",onClick:r[1]||(r[1]=z=>E("close"))})]),e("div",Fe,[e("div",Ye,[e("div",qe,[l(f,{name:"check",size:13,style:{color:"var(--good)"}}),r[6]||(r[6]=e("span",{style:{"font-weight":"600","font-size":"var(--fs-sm)"}},"Channels being edited",-1)),r[7]||(r[7]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"cyan"},{default:p(()=>[d(o(S.channels.length),1)]),_:1})]),(i(!0),u(V,null,O(S.channels.slice(0,8),z=>(i(),u("div",{key:z.id,class:"row",style:{gap:"8px",padding:"3px 0","font-size":"var(--fs-sm)"}},[e("span",Je,"#"+o(z.channel),1),e("span",We,o(z.tvg_name),1),e("span",Xe,"· "+o(z.group),1)]))),128)),S.channels.length>8?(i(),u("div",Ze," + "+o(S.channels.length-8)+" more ",1)):D("",!0)]),r[13]||(r[13]=e("div",{class:"divider"},null,-1)),e("div",Qe,[e("div",es,[r[8]||(r[8]=d(" State ",-1)),G.value?(i(),u("span",ss,"· mixed — leave unchanged")):D("",!0)]),e("div",ts,[l(ge,{value:m.value||A.value,onChange:y,options:[{value:"active",label:"Active",icon:"check"},{value:"disabled",label:"Disabled",icon:"x"}]},null,8,["value"]),m.value?(i(),T(x,{key:0,tone:m.value==="active"?"active":"disabled"},{default:p(()=>[d(o(m.value==="active"?"active":"disabled"),1)]),_:1},8,["tone"])):G.value?D("",!0):(i(),T(x,{key:1,tone:A.value==="active"?"active":"disabled"},{default:p(()=>[d(o(A.value),1)]),_:1},8,["tone"]))])]),e("div",ls,[e("div",ns,[r[9]||(r[9]=d(" Group ",-1)),I.value?(i(),u("span",as,"· mixed — leave unchanged")):D("",!0)]),e("div",os,[F(e("select",{"onUpdate:modelValue":r[2]||(r[2]=z=>g.value=z)},[e("option",is,o(I.value?"Leave unchanged (mixed)":`Leave unchanged (${M.value})`),1),(i(!0),u(V,null,O(P(fe),z=>(i(),u("option",{key:z,value:z},o(z),9,ds))),128))],512),[[ae,g.value]])])]),e("div",us,[e("div",rs,[r[10]||(r[10]=d(" Source ",-1)),c.value?(i(),u("span",vs,"· mixed — leave unchanged")):D("",!0)]),e("div",ps,[l(f,{name:"playlist",size:14}),F(e("input",{"onUpdate:modelValue":r[3]||(r[3]=z=>U.value=z),placeholder:c.value?"Leave unchanged (mixed)":`Leave unchanged (${$.value})`},null,8,cs),[[oe,U.value]])])]),e("div",ms,[r[12]||(r[12]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"ghost",onClick:r[4]||(r[4]=z=>E("close"))},{default:p(()=>[...r[11]||(r[11]=[d("Cancel",-1)])]),_:1}),l(k,{variant:"primary",icon:"check",onClick:n},{default:p(()=>[d(" Apply to "+o(S.channels.length)+" channels ",1)]),_:1})])])])]))}}),fs={class:"drawer-wrap"},ys={class:"glass drawer-panel",style:{width:"50vw","max-width":"50vw","min-width":"440px"}},xs={class:"drawer-hd"},hs={style:{flex:"1"}},bs={class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},ws={class:"drawer-body"},ks={class:"form-row"},_s={class:"row",style:{gap:"10px","align-items":"center"}},$s={class:"muted",style:{"font-size":"var(--fs-xs)"}},zs={class:"form-row"},Cs={style:{display:"grid",gap:"8px"}},Ss=["checked"],As={style:{flex:"1"}},Ps={class:"muted mono",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},Ds=["checked"],Ms={style:{flex:"1"}},Es={class:"mono",style:{padding:"0 8px 0 10px",color:"var(--text-3)","font-size":"11px","border-right":"1px solid var(--hairline)","align-self":"stretch",display:"flex","align-items":"center"}},Us={class:"form-row"},Ls={class:"row",style:{gap:"8px"}},Vs={class:"input mono",style:{flex:"1","font-size":"12px"}},Gs=["value"],Is={style:{display:"grid",gap:"8px"}},Rs={class:"row",style:{gap:"10px",padding:"8px 12px",border:"1px solid var(--hairline)","border-radius":"8px",background:"var(--bg-2)"}},Ts={class:"row",style:{gap:"10px",padding:"8px 12px",border:"1px solid var(--hairline)","border-radius":"8px",background:"var(--bg-2)"}},Os={style:{border:"1px solid var(--hairline)","border-radius":"8px",background:"var(--bg-2)",padding:"8px 12px"}},Bs={class:"row",style:{gap:"8px","margin-bottom":"6px"}},Ns={style:{"font-weight":"500"}},js={class:"mono muted",style:{"font-size":"var(--fs-xs)"}},Ks={class:"row",style:{"margin-top":"6px"}},Hs=Z({__name:"PlaylistStatusDrawer",props:{playlist:{},channels:{}},emits:["close"],setup(S,{emit:B}){const _=S,E=B,m=ye(_.playlist.id),g=h(()=>_.channels.filter(y=>y.epg==="matched").length),U=h(()=>_.channels.length-g.value),G=h(()=>{const y=new Map;for(const n of _.channels)y.set(n.group,(y.get(n.group)||0)+1);return[...y.entries()].sort((n,v)=>v[1]-n[1])}),I=h(()=>Ie(_.playlist.id)),c=h(()=>Re.value.replace(/\/$/,"")),A=b(!1);function M(){var y;try{(y=navigator.clipboard)==null||y.writeText(I.value)}catch{}A.value=!0,setTimeout(()=>A.value=!1,1400)}function $(y){m.endpointMode=y}return(y,n)=>(i(),u("div",fs,[e("div",{class:"glass-bg drawer-backdrop",onClick:n[0]||(n[0]=v=>E("close"))}),e("div",ys,[e("div",xs,[e("div",{class:K(["src-ico",{builtin:S.playlist.builtin}]),style:{width:"44px",height:"44px","border-radius":"10px"}},[l(f,{name:"globe",size:20})],2),e("div",hs,[n[7]||(n[7]=e("div",{style:{"font-weight":"600","font-size":"15px"}},"Playlist status",-1)),e("div",bs,o(S.playlist.name),1)]),l(k,{variant:"ghost",size:"sm",icon:"x",onClick:n[1]||(n[1]=v=>E("close"))})]),e("div",ws,[e("div",ks,[n[8]||(n[8]=e("div",{class:"field-lbl"},"State",-1)),e("div",_s,[l(Me,{on:P(m).active,onChange:n[2]||(n[2]=v=>P(m).active=v)},null,8,["on"]),l(x,{tone:P(m).active?"active":"disabled"},{default:p(()=>[d(o(P(m).active?"Active":"Inactive"),1)]),_:1},8,["tone"]),e("span",$s,o(P(m).active?"Playlist is being served at the endpoint below.":"Endpoint is paused. Downstream clients will receive 404."),1)])]),n[24]||(n[24]=e("div",{class:"divider"},null,-1)),e("div",zs,[n[13]||(n[13]=e("div",{class:"field-lbl"},"Endpoint",-1)),e("div",Cs,[e("label",{class:"row",style:te([{gap:"10px",padding:"8px 10px",border:"1px solid var(--hairline)","border-radius":"8px",cursor:"pointer"},P(m).endpointMode==="global"?"border-color: var(--accent); background: var(--accent-soft);":""])},[e("input",{type:"radio",name:"endpoint-mode",checked:P(m).endpointMode==="global",onChange:n[3]||(n[3]=v=>$("global"))},null,40,Ss),e("div",As,[n[9]||(n[9]=e("div",{style:{"font-weight":"500","font-size":"var(--fs-sm)"}},"Global",-1)),e("div",Ps,o(P(Ge)),1),n[10]||(n[10]=e("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},"Uses the M3U endpoint defined in Settings.",-1))])],4),e("label",{class:"row",style:te([{gap:"10px",padding:"8px 10px",border:"1px solid var(--hairline)","border-radius":"8px",cursor:"pointer","align-items":"flex-start"},P(m).endpointMode==="custom"?"border-color: var(--accent); background: var(--accent-soft);":""])},[e("input",{type:"radio",name:"endpoint-mode",checked:P(m).endpointMode==="custom",onChange:n[4]||(n[4]=v=>$("custom")),style:{"margin-top":"4px"}},null,40,Ds),e("div",Ms,[n[11]||(n[11]=e("div",{style:{"font-weight":"500","font-size":"var(--fs-sm)"}},"Custom",-1)),n[12]||(n[12]=e("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px","margin-bottom":"6px"}}," Host this playlist at a custom path on the Domain from Settings. ",-1)),e("div",{class:K(["input","mono"]),style:te([{"font-size":"12px"},P(m).endpointMode==="custom"?"":"opacity: 0.55; pointer-events: none;"])},[e("span",Es,o(c.value),1),F(e("input",{"onUpdate:modelValue":n[5]||(n[5]=v=>P(m).customPath=v),placeholder:"/playlists/my-playlist.m3u"},null,512),[[oe,P(m).customPath]])],4)])],4)])]),n[25]||(n[25]=e("div",{class:"divider"},null,-1)),e("div",Us,[n[14]||(n[14]=e("div",{class:"field-lbl"},"Hosted at",-1)),e("div",Ls,[e("div",Vs,[l(f,{name:"globe",size:14}),e("input",{value:I.value,readonly:""},null,8,Gs)]),l(k,{variant:"ghost",size:"sm",icon:A.value?"check":"copy",onClick:M},{default:p(()=>[d(o(A.value?"Copied":"Copy"),1)]),_:1},8,["icon"])])]),n[26]||(n[26]=e("div",{class:"divider"},null,-1)),n[27]||(n[27]=e("div",{class:"field-lbl",style:{"margin-bottom":"10px"}},"Summary",-1)),e("div",Is,[e("div",Rs,[l(f,{name:"check",size:13,style:{color:"var(--good)"}}),n[15]||(n[15]=e("span",{style:{"font-size":"var(--fs-sm)"}},"EPG matched",-1)),n[16]||(n[16]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"good"},{default:p(()=>[d(o(g.value),1)]),_:1})]),e("div",Ts,[l(f,{name:"warn",size:13,style:{color:"var(--warn)"}}),n[17]||(n[17]=e("span",{style:{"font-size":"var(--fs-sm)"}},"EPG unmatched",-1)),n[18]||(n[18]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"warn"},{default:p(()=>[d(o(U.value),1)]),_:1})]),e("div",Os,[e("div",Bs,[l(f,{name:"grid",size:13}),n[19]||(n[19]=e("span",{style:{"font-weight":"600","font-size":"var(--fs-sm)"}},"Channels per category",-1)),n[20]||(n[20]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"cyan"},{default:p(()=>[d(o(G.value.length),1)]),_:1})]),(i(!0),u(V,null,O(G.value,([v,r])=>(i(),u("div",{key:v,class:"row",style:{gap:"8px",padding:"3px 0","font-size":"var(--fs-sm)"}},[e("span",Ns,o(v),1),n[21]||(n[21]=e("span",{class:"spacer"},null,-1)),e("span",js,o(r),1)]))),128))])]),e("div",Ks,[n[23]||(n[23]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"primary",icon:"check",onClick:n[6]||(n[6]=v=>E("close"))},{default:p(()=>[...n[22]||(n[22]=[d("Done",-1)])]),_:1})])])])]))}}),Fs={class:"col"},Ys={class:"card",style:{display:"flex","align-items":"center",gap:"16px"}},qs={style:{flex:"1"}},Js={class:"row",style:{gap:"10px"}},Ws={style:{margin:"0","font-size":"18px","font-weight":"600"}},Xs={class:"src-url",style:{"margin-top":"4px"}},Zs={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"6px"}},Qs={class:"row",style:{gap:"18px"}},et={class:"card flush"},st={class:"toolbar"},tt={class:"select"},lt={key:0,class:"tbl"},nt={style:{width:"40px"}},at=["onClick"],ot={class:"mono muted"},it={class:"row",style:{gap:"10px"}},dt=["value","onBlur","onKeydown"],ut=["onDblclick"],rt={class:"muted"},vt={class:"mono muted"},pt={key:1,style:{color:"var(--text-3)"}},ct={class:"row",style:{gap:"6px"}},mt={class:"muted",style:{"font-size":"var(--fs-xs)"}},gt={key:1,class:"ch-grid"},ft=["onClick"],yt={class:"cbx-pos"},xt={class:"top"},ht={style:{"min-width":"0"}},bt={class:"name"},wt={class:"meta mono",style:{"margin-top":"4px"}},kt={class:"meta"},_t={class:"row"},$t={class:"modal-hd"},zt={class:"modal-body"},Ct={class:"row",style:{gap:"8px",padding:"8px 10px",background:"var(--accent-soft)","border-radius":"8px","align-items":"center"}},St={style:{"font-size":"var(--fs-sm)",color:"var(--text-1)"}},At={style:{color:"var(--accent-hi)"}},Pt={class:"form-row"},Dt={class:"input"},Mt={class:"form-row"},Et=["value"],Ut={class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"6px",display:"flex","align-items":"center",gap:"6px"}},Lt={key:0,style:{color:"var(--bad)"}},Vt={key:1,style:{color:"var(--bad)"}},Gt={class:"mono",style:{color:"var(--text-1)"}},It={style:{border:"1px solid var(--hairline)","border-radius":"10px",padding:"10px 12px",background:"var(--bg-2)","max-height":"168px",overflow:"auto"}},Rt={class:"row",style:{gap:"8px","margin-bottom":"8px"}},Tt={class:"mono muted",style:{"font-size":"var(--fs-xs)","min-width":"32px"}},Ot={style:{"font-weight":"500"}},Bt={class:"muted",style:{"font-size":"var(--fs-xs)"}},Nt={key:0,class:"muted",style:{"font-size":"var(--fs-xs)","padding-top":"6px"}},jt={class:"modal-ft"},Kt={class:"modal-hd"},Ht={class:"modal-body"},Ft={class:"row",style:{gap:"8px",padding:"8px 10px",background:"var(--accent-soft)","border-radius":"8px","align-items":"center"}},Yt={style:{"font-size":"var(--fs-sm)",color:"var(--text-1)"}},qt={style:{color:"var(--accent-hi)"}},Jt={key:0,class:"empty",style:{padding:"28px 20px","text-align":"center"}},Wt={class:"form-row"},Xt={class:"select"},Zt=["value"],Qt={key:0,style:{border:"1px solid var(--hairline)","border-radius":"10px",padding:"12px 14px",background:"var(--bg-2)",display:"grid",gap:"10px"}},el={class:"row",style:{gap:"10px"}},sl={class:"src-ico",style:{width:"40px",height:"40px","border-radius":"10px"}},tl={style:{flex:"1","min-width":"0"}},ll={style:{"font-weight":"600","font-size":"var(--fs-sm)"}},nl={class:"mono muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},al={class:"muted",style:{"font-size":"var(--fs-xs)"}},ol={class:"row",style:{gap:"10px","align-items":"center","padding-top":"8px","border-top":"1px dashed var(--hairline)"}},il={style:{color:"var(--accent-hi)"}},dl={class:"modal-ft"},ul={key:4,class:"custom-toast"},cl=Z({__name:"PlaylistDetailScreen",props:{id:{}},setup(S){const B=S,_=Ve("openChannel"),m=b({id:"",name:"…",url:"",channels:0,groups:0,lastSync:"",status:"good",auto:!1,interval:""}),g=h(()=>m.value),U=b("table"),G=b(""),I=b("all"),c=b(new Set),A=b(null),M=b([]);Ee(async()=>{const a=B.id;if(!a)return;const[s,t]=await Promise.all([fetch(`/api/playlists/${encodeURIComponent(a)}`),fetch(`/api/playlists/${encodeURIComponent(a)}/channels`)]);s.ok&&(m.value=await s.json()),t.ok&&(M.value=await t.json())});const $=b(null),y=b([...Le]),n=b(null),v=b(!1),r=b(!1),z=h(()=>ye(g.value.id)),Y=b(null);function ie(a,s){const t=s.ctrlKey||s.metaKey;if(t&&s.shiftKey){xe(a.id),Y.value=a.id;return}if(t){Q(a.id),Y.value=a.id;return}c.value.size>=2?v.value=!0:_(a)}function xe(a){const s=N.value.map(J=>J.id),t=s.indexOf(a);if(t<0)return;const w=Y.value?s.indexOf(Y.value):-1,q=w<0?t:Math.min(w,t),De=Math.max(w<0?t:w,t),pe=new Set(c.value);for(let J=q;J<=De;J++)pe.add(s[J]);c.value=pe}function he(a){if(!a.state&&!a.group&&!a.source){v.value=!1;return}const s=c.value,t=s.size;M.value=M.value.map(q=>s.has(q.id)?{...q,...a.state?{state:a.state}:{},...a.group?{group:a.group}:{},...a.source?{source:a.source}:{}}:q);const w=[];a.state&&w.push(`state → ${a.state}`),a.group&&w.push(`group → ${a.group}`),a.source&&w.push(`source → ${a.source}`),n.value={kind:"edit",text:`Updated ${t} channel${t===1?"":"s"} · ${w.join(", ")}`},v.value=!1,c.value=new Set}ce(n,a=>{a&&setTimeout(()=>n.value=null,3200)});const N=h(()=>M.value.filter(a=>(I.value==="all"||a.group===I.value)&&(G.value===""||a.tvg_name.toLowerCase().includes(G.value.toLowerCase())))),C=h(()=>M.value.filter(a=>c.value.has(a.id)));function Q(a){const s=new Set(c.value);s.has(a)?s.delete(a):s.add(a),c.value=s,Y.value=a}function be(){c.value.size===N.value.length?c.value=new Set:c.value=new Set(N.value.map(a=>a.id))}function de(a,s){M.value=M.value.map(t=>t.id===a?{...t,tvg_name:s}:t)}function we(a,s){de(a,s.target.value),A.value=null}function ke(a,s){s.key==="Enter"&&(de(a,s.target.value),A.value=null),s.key==="Escape"&&(A.value=null)}const j=b("My Custom Playlist"),ee=b(!1),L=b("my-custom-playlist");function _e(a){return a.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,48)}ce(j,a=>{ee.value||(L.value=_e(a)||"custom-playlist")});const $e=h(()=>y.value.map(a=>a.slug)),ue=h(()=>$e.value.includes(L.value)),se=h(()=>L.value.length>0&&/^[a-z0-9-]+$/.test(L.value)&&!ue.value),ze=h(()=>j.value.trim().length>0),re=h(()=>ze.value&&se.value);function Ce(){j.value="My Custom Playlist",ee.value=!1,L.value="my-custom-playlist",$.value="create"}function Se(){if(!re.value)return;const a="cust-"+L.value+"-"+Date.now().toString(36);y.value=[{id:a,name:j.value.trim(),slug:L.value,channels:C.value.length,updated:"just now"},...y.value],n.value={kind:"create",text:`Created "${j.value.trim()}" · ${C.value.length} channels`},$.value=null,c.value=new Set}const X=b("");function Ae(){var a;X.value=((a=y.value[0])==null?void 0:a.id)||"",$.value="append"}const R=h(()=>y.value.find(a=>a.id===X.value)),ve=h(()=>R.value?R.value.channels+C.value.length:0);function Pe(){R.value&&(y.value=y.value.map(a=>a.id===R.value.id?{...a,channels:a.channels+C.value.length,updated:"just now"}:a),n.value={kind:"append",text:`Appended ${C.value.length} channel${C.value.length===1?"":"s"} to "${R.value.name}"`},$.value=null,c.value=new Set)}return(a,s)=>(i(),u("div",Fs,[e("div",Ys,[e("div",{class:K(["src-ico",{builtin:g.value.builtin}]),style:{width:"52px",height:"52px","border-radius":"12px"}},[l(f,{name:g.value.builtin?"tv":"playlist",size:22},null,8,["name"])],2),e("div",qs,[e("div",Js,[e("h2",Ws,o(g.value.name),1),l(le,{status:g.value.status,pulse:g.value.status==="good"},null,8,["status","pulse"]),g.value.builtin?(i(),T(x,{key:0,tone:"system"},{default:p(()=>[l(f,{name:"check",size:10}),s[22]||(s[22]=d("built-in",-1))]),_:1})):D("",!0),l(x,{tone:"cyan"},{default:p(()=>[d(o(g.value.interval),1)]),_:1})]),e("div",Xs,o(g.value.url),1),g.value.builtin?(i(),u("div",Zs," Ships with TVApp2 · channels are preconfigured and auto-updated with the app. ")):D("",!0)]),e("div",Qs,[l(k,{variant:"ghost",icon:"globe",onClick:s[0]||(s[0]=t=>r.value=!0)},{default:p(()=>[s[23]||(s[23]=d(" Status ",-1)),l(x,{tone:z.value.active?"active":"disabled",style:{"margin-left":"6px"}},{default:p(()=>[d(o(z.value.active?"Active":"Inactive"),1)]),_:1},8,["tone"])]),_:1}),l(W,{label:"Channels",value:g.value.channels},null,8,["value"]),l(W,{label:"Groups",value:g.value.groups},null,8,["value"]),l(W,{label:"Synced",value:g.value.lastSync,small:""},null,8,["value"])]),g.value.builtin?D("",!0):(i(),T(k,{key:0,variant:"ghost",icon:"refresh"},{default:p(()=>[...s[24]||(s[24]=[d("Sync now",-1)])]),_:1})),l(k,{variant:"ghost",icon:"more"})]),e("div",et,[e("div",st,[l(Ue,{value:G.value,onChange:s[1]||(s[1]=t=>G.value=t),placeholder:"Search channels"},null,8,["value"]),e("div",tt,[F(e("select",{"onUpdate:modelValue":s[2]||(s[2]=t=>I.value=t)},[s[25]||(s[25]=e("option",{value:"all"},"All groups",-1)),(i(!0),u(V,null,O(P(fe),t=>(i(),u("option",{key:t},o(t),1))),128))],512),[[ae,I.value]])]),l(x,null,{default:p(()=>[d(o(N.value.length)+" of "+o(M.value.length),1)]),_:1}),s[31]||(s[31]=e("span",{class:"spacer"},null,-1)),c.value.size>0?(i(),u(V,{key:0},[l(x,{tone:"cyan"},{default:p(()=>[d(o(c.value.size)+" selected",1)]),_:1}),l(k,{variant:"primary",size:"sm",icon:"plus",onClick:Ce},{default:p(()=>[...s[26]||(s[26]=[d("Create",-1)])]),_:1}),l(k,{variant:"ghost",size:"sm",icon:"playlist",onClick:Ae},{default:p(()=>[...s[27]||(s[27]=[d("Append",-1)])]),_:1}),s[30]||(s[30]=e("span",{class:"tbar-sep","aria-hidden":"true"},null,-1)),l(k,{variant:"ghost",size:"sm",icon:"trash"},{default:p(()=>[...s[28]||(s[28]=[d("Delete",-1)])]),_:1}),l(k,{variant:"ghost",size:"sm",onClick:s[3]||(s[3]=t=>c.value=new Set)},{default:p(()=>[...s[29]||(s[29]=[d("Clear",-1)])]),_:1})],64)):(i(),T(ge,{key:1,value:U.value,onChange:s[4]||(s[4]=t=>U.value=t),options:[{value:"table",label:"Table",icon:"list"},{value:"grid",label:"Grid",icon:"grid"}]},null,8,["value"]))]),U.value==="table"?(i(),u("table",lt,[e("thead",null,[e("tr",null,[e("th",nt,[l(ne,{on:c.value.size>0&&c.value.size===N.value.length,onChange:be},null,8,["on"])]),s[32]||(s[32]=e("th",{style:{width:"60px"}},"#",-1)),s[33]||(s[33]=e("th",null,"Channel",-1)),s[34]||(s[34]=e("th",null,"Group",-1)),s[35]||(s[35]=e("th",null,"Source",-1)),s[36]||(s[36]=e("th",null,"TVG-ID",-1)),s[37]||(s[37]=e("th",null,"State",-1)),s[38]||(s[38]=e("th",null,"EPG",-1)),s[39]||(s[39]=e("th",{style:{width:"80px"}},"Stream",-1)),s[40]||(s[40]=e("th",{style:{width:"60px"}},null,-1))])]),e("tbody",null,[(i(!0),u(V,null,O(N.value,t=>(i(),u("tr",{key:t.id,class:K({selected:c.value.has(t.id)}),onClick:w=>ie(t,w)},[e("td",{onClick:s[5]||(s[5]=H(()=>{},["stop"]))},[l(ne,{on:c.value.has(t.id),onChange:w=>Q(t.id)},null,8,["on","onChange"])]),e("td",ot,o(t.channel),1),e("td",null,[e("div",it,[l(me,{ch:t},null,8,["ch"]),A.value===t.id?(i(),u("input",{key:0,value:t.tvg_name,onBlur:w=>we(t.id,w),onKeydown:w=>ke(t.id,w),onClick:s[6]||(s[6]=H(()=>{},["stop"])),style:{background:"var(--bg-2)",border:"1px solid var(--accent)","border-radius":"6px",padding:"3px 8px",color:"var(--text-0)","font-weight":"500",width:"200px","box-shadow":"0 0 0 3px var(--accent-soft)"}},null,40,dt)):(i(),u("span",{key:1,style:{"font-weight":"500"},onDblclick:H(w=>A.value=t.id,["stop"]),title:"Double-click to rename"},o(t.tvg_name),41,ut)),l(x,null,{default:p(()=>[d(o(t.res),1)]),_:2},1024)])]),e("td",rt,o(t.group),1),e("td",null,[l(x,{tone:"cyan"},{default:p(()=>[d(o(t.source),1)]),_:2},1024)]),e("td",vt,[t.tvg_id?(i(),u(V,{key:0},[d(o(t.tvg_id),1)],64)):(i(),u("span",pt,"—"))]),e("td",null,[l(x,{tone:t.state==="active"?"active":"disabled"},{default:p(()=>[d(o(t.state==="active"?"active":"disabled"),1)]),_:2},1032,["tone"])]),e("td",null,[t.epg==="matched"?(i(),T(x,{key:0,tone:"good"},{default:p(()=>[l(f,{name:"check",size:11}),s[41]||(s[41]=d("matched",-1))]),_:1})):(i(),T(x,{key:1,tone:"warn"},{default:p(()=>[l(f,{name:"warn",size:11}),s[42]||(s[42]=d("no match",-1))]),_:1}))]),e("td",null,[e("div",ct,[l(le,{status:t.status,pulse:t.status==="good"},null,8,["status","pulse"]),e("span",mt,o(t.status==="good"?"live":t.status==="warn"?"slow":"down"),1)])]),e("td",{onClick:s[7]||(s[7]=H(()=>{},["stop"]))},[l(k,{variant:"ghost",size:"sm",icon:"more"})])],10,at))),128))])])):(i(),u("div",gt,[(i(!0),u(V,null,O(N.value,t=>(i(),u("div",{key:t.id,class:K(["ch-card",{selected:c.value.has(t.id)}]),onClick:w=>ie(t,w)},[e("div",yt,[l(ne,{on:c.value.has(t.id),onChange:w=>Q(t.id)},null,8,["on","onChange"])]),e("div",xt,[l(me,{ch:t,size:"lg"},null,8,["ch"]),e("div",ht,[e("div",bt,o(t.tvg_name),1),e("div",wt,"#"+o(t.channel)+" · "+o(t.res),1)])]),e("div",kt,o(t.group),1),e("div",_t,[l(x,{tone:t.state==="active"?"active":"disabled"},{default:p(()=>[d(o(t.state==="active"?"active":"disabled"),1)]),_:2},1032,["tone"]),t.epg==="matched"?(i(),T(x,{key:0,tone:"good"},{default:p(()=>[l(f,{name:"check",size:11}),s[43]||(s[43]=d("EPG",-1))]),_:1})):(i(),T(x,{key:1,tone:"warn"},{default:p(()=>[...s[44]||(s[44]=[d("no EPG",-1)])]),_:1})),l(x,{tone:"cyan"},{default:p(()=>[d(o(t.source),1)]),_:2},1024),s[45]||(s[45]=e("span",{class:"spacer"},null,-1)),l(le,{status:t.status,pulse:t.status==="good"},null,8,["status","pulse"])])],10,ft))),128))]))]),$.value==="create"?(i(),u("div",{key:0,class:"modal-bg",onClick:s[13]||(s[13]=t=>$.value=null)},[e("div",{class:"modal",onClick:s[12]||(s[12]=H(()=>{},["stop"])),style:{width:"520px","max-width":"92vw"}},[e("div",$t,[l(f,{name:"plus",size:18}),s[46]||(s[46]=e("h2",null,"New custom playlist",-1)),s[47]||(s[47]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"ghost",size:"sm",icon:"x",onClick:s[8]||(s[8]=t=>$.value=null)})]),e("div",zt,[e("div",Ct,[l(f,{name:"playlist",size:13,style:{color:"var(--accent-hi)"}}),e("span",St,[e("b",At,o(C.value.length),1),d(" selected channel"+o(C.value.length===1?"":"s")+" will be added to the new playlist. ",1)])]),e("div",Pt,[s[48]||(s[48]=e("div",{class:"field-lbl"},"Playlist name",-1)),e("div",Dt,[F(e("input",{"onUpdate:modelValue":s[9]||(s[9]=t=>j.value=t),placeholder:"e.g. Saturday Football"},null,512),[[oe,j.value]])])]),e("div",Mt,[s[53]||(s[53]=e("div",{class:"field-lbl"},"URL path",-1)),e("div",{class:K(["input",{"input-bad":L.value&&!se.value}]),style:{"padding-left":"0"}},[s[49]||(s[49]=e("span",{class:"mono",style:{padding:"0 8px 0 12px",color:"var(--text-3)","font-size":"var(--fs-xs)","border-right":"1px solid var(--hairline)","align-self":"stretch",display:"flex","align-items":"center"}},"/playlists/",-1)),e("input",{class:"mono",style:{"font-size":"var(--fs-sm)"},value:L.value,onInput:s[10]||(s[10]=t=>{L.value=t.target.value.toLowerCase(),ee.value=!0}),placeholder:"my-playlist"},null,40,Et)],2),e("div",Ut,[ue.value?(i(),u("span",Lt,[l(f,{name:"x",size:11}),s[50]||(s[50]=d(" A custom playlist already uses this path.",-1))])):L.value&&!se.value?(i(),u("span",Vt,[l(f,{name:"x",size:11}),s[51]||(s[51]=d(" Use lowercase letters, numbers and dashes only.",-1))])):(i(),u(V,{key:2},[l(f,{name:"link",size:11}),s[52]||(s[52]=d(" Access at ",-1)),e("span",Gt,"https://tvapp2.example.com/playlists/"+o(L.value||"…")+".m3u",1)],64))])]),e("div",It,[e("div",Rt,[l(f,{name:"check",size:13,style:{color:"var(--good)"}}),s[54]||(s[54]=e("span",{style:{"font-weight":"600","font-size":"var(--fs-sm)"}},"Channels to include",-1)),s[55]||(s[55]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"cyan"},{default:p(()=>[d(o(C.value.length),1)]),_:1})]),(i(!0),u(V,null,O(C.value.slice(0,8),t=>(i(),u("div",{key:t.id,class:"row",style:{gap:"8px",padding:"3px 0","font-size":"var(--fs-sm)"}},[e("span",Tt,"#"+o(t.channel),1),e("span",Ot,o(t.tvg_name),1),e("span",Bt,"· "+o(t.group),1)]))),128)),C.value.length>8?(i(),u("div",Nt," + "+o(C.value.length-8)+" more ",1)):D("",!0)])]),e("div",jt,[s[58]||(s[58]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"ghost",onClick:s[11]||(s[11]=t=>$.value=null)},{default:p(()=>[...s[56]||(s[56]=[d("Cancel",-1)])]),_:1}),l(k,{variant:"primary",icon:"plus",disabled:!re.value,onClick:Se},{default:p(()=>[...s[57]||(s[57]=[d("Create playlist",-1)])]),_:1},8,["disabled"])])])])):D("",!0),$.value==="append"?(i(),u("div",{key:1,class:"modal-bg",onClick:s[18]||(s[18]=t=>$.value=null)},[e("div",{class:"modal",onClick:s[17]||(s[17]=H(()=>{},["stop"])),style:{width:"520px","max-width":"92vw"}},[e("div",Kt,[l(f,{name:"playlist",size:18}),s[59]||(s[59]=e("h2",null,"Append to custom playlist",-1)),s[60]||(s[60]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"ghost",size:"sm",icon:"x",onClick:s[14]||(s[14]=t=>$.value=null)})]),e("div",Ht,[e("div",Ft,[l(f,{name:"playlist",size:13,style:{color:"var(--accent-hi)"}}),e("span",Yt,[e("b",qt,o(C.value.length),1),d(" selected channel"+o(C.value.length===1?"":"s")+" will be appended to the playlist you choose. ",1)])]),y.value.length===0?(i(),u("div",Jt,[...s[61]||(s[61]=[e("h3",{style:{margin:"0","font-size":"var(--fs-base)"}},"No custom playlists yet",-1),e("p",{class:"muted",style:{"font-size":"var(--fs-sm)",margin:"6px 0 0"}},[d(" Use "),e("b",null,"Create"),d(" to make your first custom playlist. ")],-1)])])):(i(),u(V,{key:1},[e("div",Wt,[s[62]||(s[62]=e("div",{class:"field-lbl"},"Destination playlist",-1)),e("div",Xt,[F(e("select",{"onUpdate:modelValue":s[15]||(s[15]=t=>X.value=t)},[(i(!0),u(V,null,O(y.value,t=>(i(),u("option",{key:t.id,value:t.id},o(t.name)+" — "+o(t.channels)+" channels",9,Zt))),128))],512),[[ae,X.value]])])]),R.value?(i(),u("div",Qt,[e("div",el,[e("div",sl,[l(f,{name:"playlist",size:16})]),e("div",tl,[e("div",ll,o(R.value.name),1),e("div",nl,"/playlists/"+o(R.value.slug)+".m3u",1)]),e("div",al,"updated "+o(R.value.updated),1)]),e("div",ol,[l(W,{label:"Now",value:R.value.channels,small:""},null,8,["value"]),s[63]||(s[63]=e("span",{style:{color:"var(--text-3)","font-size":"18px"}},"→",-1)),l(W,{label:"After append",value:ve.value,small:""},{default:p(()=>[e("span",il,o(ve.value),1)]),_:1},8,["value"]),s[64]||(s[64]=e("span",{class:"spacer"},null,-1)),l(x,{tone:"cyan"},{default:p(()=>[d("+"+o(C.value.length),1)]),_:1})])])):D("",!0)],64))]),e("div",dl,[s[66]||(s[66]=e("span",{class:"spacer"},null,-1)),l(k,{variant:"ghost",onClick:s[16]||(s[16]=t=>$.value=null)},{default:p(()=>[...s[65]||(s[65]=[d("Cancel",-1)])]),_:1}),l(k,{variant:"primary",icon:"check",disabled:!R.value,onClick:Pe},{default:p(()=>[d(" Append "+o(C.value.length)+" channel"+o(C.value.length===1?"":"s"),1)]),_:1},8,["disabled"])])])])):D("",!0),v.value?(i(),T(gs,{key:2,channels:C.value,onClose:s[19]||(s[19]=t=>v.value=!1),onApply:he},null,8,["channels"])):D("",!0),r.value?(i(),T(Hs,{key:3,playlist:g.value,channels:M.value,onClose:s[20]||(s[20]=t=>r.value=!1)},null,8,["playlist","channels"])):D("",!0),n.value?(i(),u("div",ul,[l(f,{name:n.value.kind==="create"?"plus":"playlist",size:14},null,8,["name"]),e("span",null,o(n.value.text),1),e("button",{class:"custom-toast-x",onClick:s[21]||(s[21]=t=>n.value=null),"aria-label":"Dismiss"},[l(f,{name:"x",size:12})])])):D("",!0)]))}});export{cl as default}; diff --git a/dist/assets/PlaylistsScreen-0ooKY6SX.js b/dist/assets/PlaylistsScreen-0ooKY6SX.js new file mode 100644 index 00000000..5c1a7295 --- /dev/null +++ b/dist/assets/PlaylistsScreen-0ooKY6SX.js @@ -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}; diff --git a/dist/assets/SettingsScreen-Daj_a2gr.js b/dist/assets/SettingsScreen-Daj_a2gr.js new file mode 100644 index 00000000..c036e4c8 --- /dev/null +++ b/dist/assets/SettingsScreen-Daj_a2gr.js @@ -0,0 +1 @@ +import{v as ls,J as g,r,o as e,z as j,u as l,f as p,t as c,N as u,y as z,Y as A,U as H,e as y,X as v,n as ds,H as i,P as R,d as G,V as rs,D as us,m as M,B as vs,O as d,x as Q,T as cs,s as I,k as D,F as E,Z as F,K as U,q as k,l as ss,I as ps,p as T,_ as K}from"./index-CQPQcDLN.js";import{d as Y,a as h,b as es,e as ns}from"./useSettings-CPUgOpin.js";const ms={class:"endpoint-field"},ys={class:"endpoint-lbl"},fs=["readonly"],ts=ls({__name:"EndpointField",props:{label:{},icon:{},iconColor:{},defaultValue:{},modelValue:{},mono:{type:Boolean},readonly:{type:Boolean}},emits:["update:modelValue"],setup(b,{emit:_}){const $=b,L=_,P=g($.defaultValue??""),S=ds({get:()=>$.modelValue!==void 0?$.modelValue:P.value,set:f=>{$.modelValue!==void 0?L("update:modelValue",f):P.value=f}}),w=g(!1);function V(){var f;try{(f=navigator.clipboard)==null||f.writeText(S.value)}catch{}w.value=!0,setTimeout(()=>w.value=!1,1400)}return(f,C)=>(i(),r("div",ms,[e("div",ys,[e("div",{class:"endpoint-ico",style:j({color:b.iconColor||"var(--accent-hi)"})},[l(p,{name:b.icon,size:13},null,8,["name"])],4),c(" "+u(b.label),1)]),e("div",{class:z(["input",{mono:b.mono}]),style:j({flex:1,fontSize:b.mono?"12px":"var(--fs-base)"})},[l(p,{name:"globe",size:14}),A(e("input",{"onUpdate:modelValue":C[0]||(C[0]=m=>S.value=m),readonly:b.readonly},null,8,fs),[[H,S.value]])],6),l(y,{variant:"ghost",size:"sm",icon:w.value?"check":"copy",onClick:V,title:"Copy URL"},{default:v(()=>[c(u(w.value?"Copied":"Copy"),1)]),_:1},8,["icon"])]))}}),gs={class:"col",style:{"max-width":"760px"}},hs={class:"card"},bs={class:"form-grid-2"},xs={class:"form-row"},ks={class:"input"},ws={class:"form-row"},Cs={class:"input mono",style:{"font-size":"12px"}},Es={class:"form-row"},zs={class:"select"},_s={class:"card"},$s={class:"sync-grid"},Ps={class:"sync-row sync-row-lvl"},Ss={class:"sync-col-toggle"},Vs={class:"muted",style:{"font-size":"var(--fs-xs)","font-weight":"400","margin-left":"6px"}},Rs={class:"sync-meta"},Gs={class:"sync-lbl row",style:{gap:"8px"}},Us={class:"sync-hint",style:{display:"flex","align-items":"center",gap:"5px"}},Bs={style:{color:"var(--text-1)","font-weight":"500"}},Ds={class:"sync-col-sched"},Ts={key:0,class:"input mono",style:{height:"30px",width:"100%","font-size":"11px"}},Ls=["value","onBlur","onKeydown"],Ns={key:1,class:"select",style:{width:"100%"}},As=["value","onChange"],Ms=["value"],Is=["value"],Fs={class:"sync-col-cron"},Ks=["onClick"],Ys={class:"sync-col-toggle"},Hs={class:"muted",style:{"font-size":"var(--fs-xs)","font-weight":"400","margin-left":"6px"}},Ws={class:"sync-meta"},Os={class:"sync-lbl row",style:{gap:"8px"}},qs={class:"sync-hint",style:{display:"flex","align-items":"center",gap:"5px"}},Js={style:{color:"var(--text-1)","font-weight":"500"}},Xs={class:"sync-col-sched"},Zs={key:0,class:"input mono",style:{height:"30px",width:"100%","font-size":"11px"}},js=["value","onBlur","onKeydown"],Qs={key:1,class:"select",style:{width:"100%"}},se=["value","onChange"],ee=["value"],ne=["value"],te={class:"sync-col-cron"},le=["onClick"],oe={class:"sync-col-toggle"},ie={class:"sync-row sync-row-lvl"},ae={class:"sync-col-toggle"},de={class:"sync-row sync-row-lvl sync-row-last"},re={class:"sync-col-toggle"},ue={class:"card"},ve={class:"row"},ce={class:"modal-hd"},pe={class:"modal-body"},me={style:{display:"grid",gap:"10px"}},ye={class:"row",style:{gap:"8px","margin-bottom":"8px"}},fe={style:{"font-weight":"600","font-size":"var(--fs-sm)"}},ge={style:{"font-weight":"500"}},he={class:"mono muted",style:{"font-size":"var(--fs-xs)"}},be={class:"row",style:{gap:"8px",padding:"8px 10px",background:"var(--accent-soft)","border-radius":"8px","align-items":"flex-start"}},xe={style:{color:"var(--accent-hi)","margin-top":"1px"}},ke={class:"modal-ft"},Ee=ls({__name:"SettingsScreen",setup(b){const _=[{label:"Every 15 minutes",cron:"*/15 * * * *",next:"in 8 min"},{label:"Every 30 minutes",cron:"*/30 * * * *",next:"in 22 min"},{label:"Every hour",cron:"0 * * * *",next:"in 38 min"},{label:"Every 6 hours",cron:"0 */6 * * *",next:"in 3h 12m"},{label:"Every 12 hours",cron:"0 */12 * * *",next:"in 8h 41m"},{label:"Daily at 03:00",cron:"0 3 * * *",next:"tomorrow 03:00"},{label:"Daily at 06:00",cron:"0 6 * * *",next:"tomorrow 06:00"},{label:"Weekly (Sun 04:00)",cron:"0 4 * * 0",next:"Sun 04:00"}];function $(t){return t.interval==="Every 6 hours"?"0 */6 * * *":t.interval==="Every 12 hours"?"0 */12 * * *":t.interval==="Daily"?"0 3 * * *":t.interval==="Auto-updated"?"0 */6 * * *":"0 3 * * *"}function L(t){var s;return((s=_.find(n=>n.cron===t))==null?void 0:s.next)||"—"}const P=g(!0),S=g(!0),w=g(!1),V=g("Europe/London"),f=g(!1),C=g(!1),m=g("idle"),x=g(null),W=R.filter(t=>t.builtin).length,O=G.filter(t=>t.builtin).length,a=ps({});[...R,...G].forEach(t=>{a[t.id]={cron:$(t),enabled:!0}});function B(t,s){a[t]={...a[t],...s}}rs(m,t=>{t==="done"&&setTimeout(()=>m.value="idle",2200)});function q(){m.value="done"}us(()=>M.on("tvapp:restore-done",q)),vs(()=>M.off("tvapp:restore-done",q));function os(){const t=[];R.filter(s=>s.builtin).forEach(s=>{t.push({kind:"playlist",text:`Fetching playlist · ${s.name}`}),t.push({kind:"playlist",text:`Indexing channels · ${s.name}`})}),G.filter(s=>s.builtin).forEach(s=>{t.push({kind:"epg",text:`Downloading EPG · ${s.name}`}),t.push({kind:"epg",text:`Parsing programmes · ${s.name}`})}),t.push({kind:"refresh",text:"Rebuilding workspace cache"}),M.emit("tvapp:restore-start",{items:t}),m.value="restoring"}function N(t){return _.some(s=>s.cron===t)}function J(t,s){if(s==="__custom"){x.value=t;return}B(t,{cron:s})}function X(t,s){B(t,{cron:s.target.value}),x.value=null}function Z(t,s){s.key==="Enter"&&(B(t,{cron:s.target.value}),x.value=null),s.key==="Escape"&&(x.value=null)}const is=R.filter(t=>t.builtin),as=G.filter(t=>t.builtin);return(t,s)=>(i(),r("div",gs,[e("div",hs,[s[23]||(s[23]=e("h3",{class:"section-title"},"General",-1)),e("div",bs,[e("div",xs,[s[18]||(s[18]=e("div",{class:"field-lbl"},"Display name",-1)),e("div",ks,[A(e("input",{"onUpdate:modelValue":s[0]||(s[0]=n=>Q(Y)?Y.value=n:null)},null,512),[[H,d(Y)]])])]),e("div",ws,[s[19]||(s[19]=e("div",{class:"field-lbl"},"Domain",-1)),e("div",Cs,[l(p,{name:"globe",size:14}),A(e("input",{"onUpdate:modelValue":s[1]||(s[1]=n=>Q(h)?h.value=n:null),placeholder:"https://tvapp2.example.com"},null,512),[[H,d(h)]])]),s[20]||(s[20]=e("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"6px"}}," Base URL used by all hosted endpoints (M3U, EPG, per-playlist custom paths). ",-1))]),e("div",Es,[s[22]||(s[22]=e("div",{class:"field-lbl"},"Time zone",-1)),e("div",zs,[A(e("select",{"onUpdate:modelValue":s[2]||(s[2]=n=>V.value=n)},[...s[21]||(s[21]=[e("option",null,"Europe/London",-1),e("option",null,"America/New_York",-1),e("option",null,"Europe/Berlin",-1),e("option",null,"Asia/Tokyo",-1)])],512),[[cs,V.value]])])])]),s[24]||(s[24]=e("div",{class:"divider",style:{margin:"18px 0 14px"}},null,-1)),s[25]||(s[25]=e("div",{class:"field-lbl",style:{"margin-bottom":"10px"}},"Hosting endpoints",-1)),s[26]||(s[26]=e("div",{class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"-4px","margin-bottom":"12px"}}," Public URLs where TVApp2 will expose the consolidated M3U playlist and EPG guide to your downstream apps. ",-1)),l(ts,{label:"M3U endpoint",icon:"playlist","model-value":`${d(h).replace(/\/$/,"")}${d(es)}`,"onUpdate:modelValue":s[3]||(s[3]=n=>es.value=n.startsWith(d(h).replace(/\/$/,""))?n.slice(d(h).replace(/\/$/,"").length):n),mono:""},null,8,["model-value"]),s[27]||(s[27]=e("div",{style:{height:"10px"}},null,-1)),l(ts,{label:"EPG endpoint",icon:"epg","icon-color":"var(--good)","model-value":`${d(h).replace(/\/$/,"")}${d(ns)}`,"onUpdate:modelValue":s[4]||(s[4]=n=>ns.value=n.startsWith(d(h).replace(/\/$/,""))?n.slice(d(h).replace(/\/$/,"").length):n),mono:""},null,8,["model-value"])]),e("div",_s,[s[43]||(s[43]=e("h3",{class:"section-title"},"Syncing",-1)),e("div",$s,[e("div",Ps,[s[28]||(s[28]=I('
Auto-sync sources
Refresh all playlists and EPG sources on their configured schedules.
',3)),e("div",Ss,[l(D,{on:P.value,onChange:s[5]||(s[5]=n=>P.value=n)},null,8,["on"])])]),P.value?(i(),r(E,{key:0},[e("div",{class:z(["sync-section-hd collapsible",{open:f.value}]),onClick:s[7]||(s[7]=n=>f.value=!f.value)},[l(p,{name:"chevron-r",size:12,class:"chev"}),l(p,{name:"playlist",size:13}),s[30]||(s[30]=e("span",null,"Playlists",-1)),e("span",Vs,u(d(R).length)+" sources · "+u(V.value),1),s[31]||(s[31]=e("span",{class:"spacer"},null,-1)),l(y,{variant:"ghost",size:"sm",icon:"refresh",onClick:s[6]||(s[6]=F(()=>{},["stop"]))},{default:v(()=>[...s[29]||(s[29]=[c("Sync all",-1)])]),_:1})],2),f.value?(i(!0),r(E,{key:0},U(d(R),n=>(i(),r("div",{key:n.id,class:z(["sync-row sync-row-sched",{disabled:!a[n.id].enabled}])},[e("div",{class:z(["sched-ico",{builtin:n.builtin}])},[l(p,{name:n.builtin?"tv":"playlist",size:14},null,8,["name"])],2),e("div",Rs,[e("div",Gs,[c(u(n.name)+" ",1),n.builtin?(i(),T(K,{key:0,tone:"system"},{default:v(()=>[l(p,{name:"check",size:10}),s[32]||(s[32]=c("built-in",-1))]),_:1})):k("",!0)]),e("div",Us,[l(p,{name:"refresh",size:11}),s[33]||(s[33]=c(" Next run ",-1)),e("b",Bs,u(L(a[n.id].cron)),1)])]),e("div",Ds,[x.value===n.id?(i(),r("div",Ts,[e("input",{value:a[n.id].cron,onBlur:o=>X(n.id,o),onKeydown:o=>Z(n.id,o)},null,40,Ls)])):(i(),r("div",Ns,[e("select",{value:N(a[n.id].cron)?a[n.id].cron:"__custom",onChange:o=>J(n.id,o.target.value)},[(i(),r(E,null,U(_,o=>e("option",{key:o.cron,value:o.cron},u(o.label),9,Ms)),64)),s[34]||(s[34]=e("option",{value:"__custom"},"Custom…",-1)),N(a[n.id].cron)?k("",!0):(i(),r("option",{key:0,value:a[n.id].cron},"Custom: "+u(a[n.id].cron),9,Is))],40,As)]))]),e("div",Fs,[e("code",{class:"cron-chip",onClick:o=>x.value=n.id,title:"Click to edit cron"},u(a[n.id].cron),9,Ks)]),e("div",Ys,[l(D,{on:a[n.id].enabled,onChange:o=>B(n.id,{enabled:o})},null,8,["on","onChange"])])],2))),128)):k("",!0),e("div",{class:z(["sync-section-hd collapsible",{open:C.value}]),onClick:s[9]||(s[9]=n=>C.value=!C.value)},[l(p,{name:"chevron-r",size:12,class:"chev"}),l(p,{name:"epg",size:13,style:{color:"var(--good)"}}),s[36]||(s[36]=e("span",null,"EPG Sources",-1)),e("span",Hs,u(d(G).length)+" sources · "+u(V.value),1),s[37]||(s[37]=e("span",{class:"spacer"},null,-1)),l(y,{variant:"ghost",size:"sm",icon:"refresh",onClick:s[8]||(s[8]=F(()=>{},["stop"]))},{default:v(()=>[...s[35]||(s[35]=[c("Sync all",-1)])]),_:1})],2),C.value?(i(!0),r(E,{key:1},U(d(G),n=>(i(),r("div",{key:n.id,class:z(["sync-row sync-row-sched",{disabled:!a[n.id].enabled}])},[e("div",{class:z(["sched-ico is-epg",{builtin:n.builtin,"epg-builtin":n.builtin}])},[l(p,{name:n.builtin?"tv":"epg",size:14},null,8,["name"])],2),e("div",Ws,[e("div",Os,[c(u(n.name)+" ",1),n.builtin?(i(),T(K,{key:0,tone:"system"},{default:v(()=>[l(p,{name:"check",size:10}),s[38]||(s[38]=c("built-in",-1))]),_:1})):k("",!0)]),e("div",qs,[l(p,{name:"refresh",size:11}),s[39]||(s[39]=c(" Next run ",-1)),e("b",Js,u(L(a[n.id].cron)),1)])]),e("div",Xs,[x.value===n.id?(i(),r("div",Zs,[e("input",{value:a[n.id].cron,onBlur:o=>X(n.id,o),onKeydown:o=>Z(n.id,o)},null,40,js)])):(i(),r("div",Qs,[e("select",{value:N(a[n.id].cron)?a[n.id].cron:"__custom",onChange:o=>J(n.id,o.target.value)},[(i(),r(E,null,U(_,o=>e("option",{key:o.cron,value:o.cron},u(o.label),9,ee)),64)),s[40]||(s[40]=e("option",{value:"__custom"},"Custom…",-1)),N(a[n.id].cron)?k("",!0):(i(),r("option",{key:0,value:a[n.id].cron},"Custom: "+u(a[n.id].cron),9,ne))],40,se)]))]),e("div",te,[e("code",{class:"cron-chip",onClick:o=>x.value=n.id,title:"Click to edit cron"},u(a[n.id].cron),9,le)]),e("div",oe,[l(D,{on:a[n.id].enabled,onChange:o=>B(n.id,{enabled:o})},null,8,["on","onChange"])])],2))),128)):k("",!0)],64)):k("",!0),e("div",ie,[s[41]||(s[41]=I('
Auto-match channels to EPG
Run fuzzy name matching against EPG channel IDs after each import.
',3)),e("div",ae,[l(D,{on:S.value,onChange:s[10]||(s[10]=n=>S.value=n)},null,8,["on"])])]),e("div",de,[s[42]||(s[42]=I('
Email me about sync failures
Send a summary if more than 3% of channels go offline.
',3)),e("div",re,[l(D,{on:w.value,onChange:s[11]||(s[11]=n=>w.value=n)},null,8,["on"])])])])]),e("div",ue,[s[60]||(s[60]=e("h3",{class:"section-title"},"Data",-1)),e("div",ve,[l(y,{variant:"ghost",icon:"upload"},{default:v(()=>[...s[44]||(s[44]=[c("Export all sources",-1)])]),_:1}),l(y,{variant:"ghost",icon:"refresh"},{default:v(()=>[...s[45]||(s[45]=[c("Rebuild EPG index",-1)])]),_:1}),l(y,{variant:"ghost",icon:"trash"},{default:v(()=>[...s[46]||(s[46]=[c("Clear cache",-1)])]),_:1})]),s[61]||(s[61]=e("div",{class:"divider"},null,-1)),l(ss,{label:"Restore built-in sources",hint:`Re-add the ${d(W)} default playlist${d(W)===1?"":"s"} and ${d(O)} default EPG source${d(O)===1?"":"s"} that ship with TVApp2. Your custom sources are untouched.`},{right:v(()=>[m.value==="done"?(i(),T(y,{key:0,variant:"ghost",icon:"check"},{default:v(()=>[...s[47]||(s[47]=[e("span",{style:{color:"var(--good)"}},"Restored",-1)])]),_:1})):m.value==="restoring"?(i(),T(y,{key:1,variant:"ghost",icon:"refresh",disabled:""},{default:v(()=>[...s[48]||(s[48]=[e("span",{class:"muted"},"Restoring…",-1)])]),_:1})):(i(),T(y,{key:2,variant:"ghost",icon:"refresh",onClick:s[12]||(s[12]=n=>m.value="confirm")},{default:v(()=>[...s[49]||(s[49]=[c("Restore defaults",-1)])]),_:1}))]),_:1},8,["hint"]),m.value==="confirm"?(i(),r("div",{key:0,class:"modal-bg",onClick:s[17]||(s[17]=n=>m.value="idle")},[e("div",{class:"modal",onClick:s[16]||(s[16]=F(()=>{},["stop"])),style:{width:"520px","max-width":"92vw"}},[e("div",ce,[l(p,{name:"refresh",size:18}),s[50]||(s[50]=e("h2",null,"Restore built-in sources?",-1)),s[51]||(s[51]=e("span",{class:"spacer"},null,-1)),l(y,{variant:"ghost",size:"sm",icon:"x",onClick:s[13]||(s[13]=n=>m.value="idle")})]),e("div",pe,[s[55]||(s[55]=e("div",{style:{"font-size":"var(--fs-base)",color:"var(--text-1)","line-height":"1.5"}}," TVApp2 will re-add the default playlist and EPG sources that ship with the app. If any of them were previously hidden or removed, they will reappear in your workspace. ",-1)),e("div",me,[(i(!0),r(E,null,U([{title:"Playlists",icon:"playlist",items:d(is)},{title:"EPG sources",icon:"epg",items:d(as)}],n=>(i(),r("div",{key:n.title,style:{border:"1px solid var(--hairline)","border-radius":"10px",padding:"10px 12px",background:"var(--bg-2)"}},[e("div",ye,[l(p,{name:n.icon,size:14},null,8,["name"]),e("span",fe,u(n.title),1),s[52]||(s[52]=e("span",{class:"spacer"},null,-1)),l(K,{tone:"cyan"},{default:v(()=>[c(u(n.items.length),1)]),_:2},1024)]),(i(!0),r(E,null,U(n.items,o=>(i(),r("div",{key:o.id,class:"row",style:{gap:"8px",padding:"4px 0","font-size":"var(--fs-sm)"}},[s[53]||(s[53]=e("span",{class:"dot good",style:{width:"6px",height:"6px"}},null,-1)),e("span",ge,u(o.name),1),e("span",he,u(o.url),1)]))),128))]))),128))]),e("div",be,[e("span",xe,[l(p,{name:"check",size:13})]),s[54]||(s[54]=e("span",{style:{"font-size":"var(--fs-xs)",color:"var(--text-1)","line-height":"1.5"}}," Your custom playlists, EPG sources, channel mappings, and viewing history will not be modified. ",-1))])]),e("div",ke,[s[58]||(s[58]=e("span",{class:"spacer"},null,-1)),l(y,{variant:"ghost",onClick:s[14]||(s[14]=n=>m.value="idle")},{default:v(()=>[...s[56]||(s[56]=[c("Cancel",-1)])]),_:1}),l(y,{variant:"primary",icon:"refresh",onClick:s[15]||(s[15]=n=>{os(),m.value="idle"})},{default:v(()=>[...s[57]||(s[57]=[c("Confirm restore",-1)])]),_:1})])])])):k("",!0),s[62]||(s[62]=e("div",{class:"divider"},null,-1)),l(ss,{label:"Danger zone",hint:"Permanently delete all playlists, EPG data, and mappings."},{right:v(()=>[l(y,{variant:"ghost",icon:"trash"},{default:v(()=>[...s[59]||(s[59]=[e("span",{style:{color:"var(--bad)"}},"Reset workspace",-1)])]),_:1})]),_:1})])]))}});export{Ee as default}; diff --git a/dist/assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js b/dist/assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js new file mode 100644 index 00000000..92fed23a --- /dev/null +++ b/dist/assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js @@ -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 _}; diff --git a/dist/assets/index-BzPI8e-F.css b/dist/assets/index-BzPI8e-F.css new file mode 100644 index 00000000..ec58ed57 --- /dev/null +++ b/dist/assets/index-BzPI8e-F.css @@ -0,0 +1 @@ +.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;max-height:calc(100vh - 32px);display:flex;flex-direction:column;background:#faf9f7c7;color:#29261b;-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);border:.5px solid rgba(255,255,255,.6);border-radius:14px;box-shadow:0 1px #ffffff80 inset,0 12px 40px #0000002e;font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}.twk-hd{display:flex;align-items:center;justify-content:space-between;padding:10px 8px 10px 14px;cursor:move;-webkit-user-select:none;user-select:none}.twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}.twk-x{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;background:transparent;color:#29261b8c;width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}.twk-x:hover{background:#0000000f;color:#29261b}.twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;overflow-y:auto;overflow-x:hidden;min-height:0}.twk-row{display:flex;flex-direction:column;gap:5px}.twk-lbl{display:flex;justify-content:space-between;align-items:baseline;color:#29261bb8}.twk-lbl>span:first-child{font-weight:500}.twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#29261b73;padding:10px 0 0}.twk-sect:first-child{padding-top:0}.twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;background:#0000000f;-webkit-user-select:none;user-select:none}.twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;background:#ffffffe6;box-shadow:0 1px 2px #0000001f;transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}.twk-seg button{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;z-index:1;flex:1;border:0;background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2}:root{--bg-0: oklch(.16 .005 240);--bg-1: oklch(.2 .006 240);--bg-2: oklch(.24 .008 240);--bg-3: oklch(.28 .01 240);--hairline: oklch(1 0 0 / .07);--hairline-strong: oklch(1 0 0 / .12);--text-0: oklch(.98 0 0);--text-1: oklch(.78 .01 240);--text-2: oklch(.58 .012 240);--text-3: oklch(.42 .012 240);--accent: oklch(.82 .13 220);--accent-hi: oklch(.88 .13 220);--accent-soft: oklch(.82 .13 220 / .14);--accent-glow: oklch(.82 .13 220 / .35);--good: oklch(.78 .16 150);--warn: oklch(.82 .15 80);--bad: oklch(.7 .18 25);--radius-s: 8px;--radius-m: 12px;--radius-l: 16px;--row-h: 44px;--pad-card: 18px;--pad-x: 16px;--gap: 14px;--fs-base: 13.5px;--fs-sm: 12px;--fs-xs: 11px;--fs-h: 22px;--fs-h2: 16px}[data-density=compact]{--row-h: 36px;--pad-card: 14px;--pad-x: 12px;--gap: 10px;--fs-base: 12.5px;--fs-sm: 11.5px;--fs-xs: 10.5px;--fs-h: 19px;--fs-h2: 14.5px}[data-density=spacious]{--row-h: 52px;--pad-card: 22px;--pad-x: 20px;--gap: 18px;--fs-base: 14.5px;--fs-sm: 13px;--fs-xs: 11.5px;--fs-h: 26px;--fs-h2: 18px}[data-theme=light]{--bg-0: oklch(.985 .003 240);--bg-1: oklch(1 0 0);--bg-2: oklch(.965 .004 240);--bg-3: oklch(.94 .005 240);--hairline: oklch(0 0 0 / .08);--hairline-strong: oklch(0 0 0 / .14);--text-0: oklch(.18 .01 240);--text-1: oklch(.32 .012 240);--text-2: oklch(.5 .012 240);--text-3: oklch(.62 .012 240);--accent: oklch(.52 .15 220);--accent-hi: oklch(.44 .16 220);--accent-soft: oklch(.52 .15 220 / .12);--accent-glow: oklch(.52 .15 220 / .22);--good: oklch(.48 .17 150);--warn: oklch(.55 .17 60);--bad: oklch(.52 .2 25)}[data-theme=light] .log-line.log-error .log-msg{color:#b2001b;color:oklch(.48 .2 25)}[data-theme=light] .live-pill{color:#b2001b;color:oklch(.48 .2 25);background:#c217251f;border-color:#c2172566}[data-theme=light] .src-row .src-ico.builtin,[data-theme=light] .sched-ico.builtin{background:var(--bg-3);border-color:#00758e4d;border-color:oklch(.52 .15 220 / .3);box-shadow:inset 0 0 0 1px #00758e26,0 0 18px #00758e2e;box-shadow:inset 0 0 0 1px oklch(.52 .15 220 / .15),0 0 18px oklch(.52 .15 220 / .18)}[data-theme=light] .src-row .src-ico.builtin.epg-builtin,[data-theme=light] .src-ico.builtin.epg-builtin,[data-theme=light] .sched-ico.builtin.epg-builtin{background:var(--bg-3);border-color:#0071314d;border-color:oklch(.48 .17 150 / .3);box-shadow:inset 0 0 0 1px #00713126,0 0 18px #0071312e;box-shadow:inset 0 0 0 1px oklch(.48 .17 150 / .15),0 0 18px oklch(.48 .17 150 / .18)}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,Helvetica Neue,sans-serif;font-size:var(--fs-base);color:var(--text-0);background:var(--bg-0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;letter-spacing:-.005em}button,input,textarea,select{font:inherit;color:inherit}button,input,textarea,select{outline:none}::selection{background:var(--accent-soft);color:var(--text-0)}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background:var(--hairline-strong);border-radius:6px;border:2px solid transparent;background-clip:content-box}*::-webkit-scrollbar-thumb:hover{background:var(--text-3);background-clip:content-box;border:2px solid transparent}.app{display:grid;grid-template-columns:224px 1fr;height:100vh;min-width:1100px}.sidebar{background:var(--bg-0);border-right:1px solid var(--hairline);padding:20px 14px;display:flex;flex-direction:column;gap:18px;overflow:hidden}.brand{padding:4px 8px 8px;font-weight:700;font-size:19px;letter-spacing:-.02em;color:var(--accent);text-shadow:0 0 24px var(--accent-glow);display:flex;align-items:center;gap:8px}.brand-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)}.nav-group-label{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--text-3);padding:6px 10px;margin-top:6px}.nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius-s);color:var(--text-1);cursor:default;font-size:var(--fs-base);transition:background .12s,color .12s;-webkit-user-select:none;user-select:none}.nav-item:hover{background:var(--bg-1);color:var(--text-0)}.nav-item.active{background:var(--accent-soft);color:var(--accent-hi);box-shadow:inset 0 0 0 1px #48d7fe40}.nav-item .ico{width:16px;height:16px;flex:none;opacity:.9}.nav-item .count{margin-left:auto;font-size:11px;font-variant-numeric:tabular-nums;color:var(--text-3);background:var(--bg-1);padding:1px 7px;border-radius:999px}.nav-item.active .count{background:#48d7fe33;color:var(--accent-hi)}.sidebar-foot{margin-top:auto;display:flex;align-items:center;gap:10px;padding:10px;border-radius:var(--radius-s);background:var(--bg-1);border:1px solid var(--hairline)}.avatar{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,#48d7fe,#65a4f4,#6f6de8);flex:none;display:grid;place-items:center;color:#fff;font-weight:700;font-size:11px}.sidebar-foot .name{font-size:12.5px;color:var(--text-0)}.sidebar-foot .plan{font-size:10.5px;color:var(--text-2)}.main{display:flex;flex-direction:column;background:var(--bg-0);overflow:hidden}.topbar{height:56px;flex:none;border-bottom:1px solid var(--hairline);display:flex;align-items:center;padding:0 var(--pad-x);gap:12px}.topbar h1{margin:0;font-size:var(--fs-h);font-weight:600;letter-spacing:-.018em}.topbar .crumb{color:var(--text-2);font-size:var(--fs-sm)}.topbar-spacer{flex:1}.restore-strip{flex:1;min-width:0;display:grid;grid-template-columns:1fr auto;grid-template-rows:auto auto;column-gap:14px;row-gap:6px;align-items:center;margin:0 18px 0 22px;animation:fade-in .22s ease-out}.restore-strip-line{grid-column:1;grid-row:1;display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);color:var(--text-1);white-space:nowrap;overflow:hidden;min-width:0}.restore-strip-line>svg{color:var(--accent-hi);flex:none;filter:drop-shadow(0 0 6px var(--accent-glow))}.restore-strip-action{color:var(--accent-hi);font-weight:600;letter-spacing:.01em;flex:none}.restore-strip-label{color:var(--text-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1}.restore-strip-bar{grid-column:1;grid-row:2;height:4px;background:var(--hairline-strong);border-radius:999px;position:relative;overflow:hidden}.restore-strip-fill{position:absolute;inset:0 auto 0 0;border-radius:999px;background:linear-gradient(90deg,var(--accent) 0%,var(--accent-hi) 60%,var(--accent) 100%);background-size:200% 100%;box-shadow:0 0 10px var(--accent),0 0 22px var(--accent-glow);transition:width .26s cubic-bezier(.4,.7,.3,1);animation:restore-shimmer 1.6s linear infinite}.restore-strip-fill:after{content:"";position:absolute;top:0;right:0;bottom:0;width:18px;background:linear-gradient(90deg,transparent,oklch(.98 .05 220 / .85));filter:blur(2px)}@keyframes restore-shimmer{0%{background-position:0% 50%}to{background-position:200% 50%}}.restore-strip-pct{grid-column:2;grid-row:1 / span 2;align-self:center;font-size:12px;font-weight:600;color:var(--accent-hi);letter-spacing:.02em;text-shadow:0 0 12px var(--accent-glow);font-variant-numeric:tabular-nums;min-width:38px;text-align:right}.theme-toggle{position:relative;display:inline-flex;align-items:center;gap:0;width:56px;height:28px;padding:0;border-radius:999px;border:1px solid var(--hairline);background:var(--bg-2);cursor:pointer;transition:background .18s ease,border-color .18s ease}.theme-toggle:hover{border-color:var(--hairline-strong)}.theme-toggle-ico{flex:1;display:grid;place-items:center;color:var(--text-3);z-index:1;pointer-events:none}.theme-toggle-thumb{position:absolute;top:2px;width:24px;height:24px;border-radius:50%;display:grid;place-items:center;background:var(--bg-0);color:var(--accent-hi);border:1px solid var(--hairline-strong);box-shadow:0 1px 2px #00000040,0 0 12px var(--accent-glow);transition:left .22s cubic-bezier(.4,.7,.3,1),background .18s ease;z-index:2}.theme-toggle-thumb.is-light{left:2px}.theme-toggle-thumb.is-dark{left:30px}.screen{flex:1;overflow-y:auto;padding:var(--pad-x)}.card{background:var(--bg-1);border:1px solid var(--hairline);border-radius:var(--radius-m);padding:var(--pad-card)}.card.flush{padding:0}.card-hd{display:flex;align-items:center;gap:10px;padding:14px var(--pad-card);border-bottom:1px solid var(--hairline)}.card-hd h2{margin:0;font-size:var(--fs-h2);font-weight:600;letter-spacing:-.01em}.btn{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 14px;border-radius:var(--radius-s);border:1px solid var(--hairline);background:var(--bg-1);color:var(--text-0);cursor:default;font-size:var(--fs-sm);font-weight:500;white-space:nowrap;transition:background .12s,border-color .12s,color .12s,box-shadow .12s;-webkit-user-select:none;user-select:none}.btn:hover{background:var(--bg-2);border-color:var(--hairline-strong)}.btn:disabled,.btn[disabled]{opacity:.42;cursor:not-allowed;pointer-events:none;filter:saturate(.4);box-shadow:none}.btn .ico{width:14px;height:14px;flex:none}.btn.primary{background:linear-gradient(180deg,#94e5ff,#57daff 50%,#30d3fc,#12cbf5);background:linear-gradient(180deg,color(xyz 0.56 0.706 1.226),color(xyz 0.465 0.594 1.076) 50%,#30d3fc,#12cbf5);color:#001828;color:oklch(.2 .05 240);border:1px solid oklch(.88 .16 220 / .6);box-shadow:0 0 0 1px #bef6 inset,0 0 24px #48d7fe73,0 1px #fff6 inset;box-shadow:0 0 0 1px oklch(.92 .1 220 / .4) inset,0 0 24px #48d7fe73,0 1px #fff6 inset;font-weight:600}.btn.primary:hover{filter:brightness(1.05)}.btn.ghost{background:transparent;border-color:transparent;color:var(--text-1)}.btn.ghost:hover{background:var(--bg-1);color:var(--text-0)}.btn.icon{padding:0;width:34px;justify-content:center}.btn.danger{color:var(--bad)}.btn.danger:hover{background:#fa68631a;border-color:#fa68634d}.btn.sm{height:28px;padding:0 10px;font-size:var(--fs-xs)}.btn.sm.icon{width:28px;padding:0}.input{display:flex;align-items:center;gap:8px;height:34px;padding:0 10px;border-radius:var(--radius-s);border:1px solid var(--hairline);background:var(--bg-1);color:var(--text-0);transition:border-color .12s,box-shadow .12s}.input:focus-within{border-color:#48d7fe80;box-shadow:0 0 0 3px var(--accent-soft)}.input input,.input textarea{flex:1;background:transparent;border:0;padding:0;min-width:0}.input input::placeholder,.input textarea::placeholder{color:var(--text-3)}.input .ico{width:14px;height:14px;color:var(--text-2);flex:none}.input.lg{height:38px}textarea.input{height:auto;padding:10px;min-height:80px;resize:vertical}.select{position:relative}.select select{-moz-appearance:none;appearance:none;-webkit-appearance:none;height:34px;padding:0 28px 0 10px;border-radius:var(--radius-s);border:1px solid var(--hairline);background:var(--bg-1);color:var(--text-0);font-size:var(--fs-sm)}.select:after{content:"";position:absolute;right:10px;top:50%;width:8px;height:8px;border-right:1.5px solid var(--text-2);border-bottom:1.5px solid var(--text-2);transform:translateY(-70%) rotate(45deg);pointer-events:none}.toggle{width:38px;height:22px;border-radius:999px;background:var(--bg-3);border:1px solid var(--hairline);position:relative;cursor:default;flex:none;transition:background .15s,box-shadow .15s,border-color .15s}.toggle:after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#cecece;box-shadow:0 1px 2px #0006;transition:left .15s,background .15s}.toggle.on{background:var(--accent);border-color:#95e5ff;border-color:oklch(.88 .16 220);box-shadow:0 0 20px var(--accent-glow)}.toggle.on:after{left:18px;background:#fff}.cbx{width:18px;height:18px;border-radius:5px;border:1.5px solid var(--hairline-strong);background:var(--bg-1);flex:none;display:grid;place-items:center;cursor:default;transition:background .12s,border-color .12s,box-shadow .12s}.cbx.on{background:var(--accent);border-color:var(--accent);box-shadow:0 0 14px var(--accent-glow)}.cbx.on:after{content:"";width:5px;height:9px;border-right:2px solid oklch(.2 .05 240);border-bottom:2px solid oklch(.2 .05 240);transform:rotate(45deg) translate(-1px,-1px)}.dot{width:8px;height:8px;border-radius:50%;flex:none;position:relative}.dot.good{background:var(--good);box-shadow:0 0 8px #5fd37f99}.dot.warn{background:var(--warn);box-shadow:0 0 8px #f7b83d99}.dot.bad{background:var(--bad);box-shadow:0 0 8px #fa686399}.dot.idle{background:var(--text-3)}.dot.pulse:before{content:"";position:absolute;top:-3px;right:-3px;bottom:-3px;left:-3px;border-radius:50%;background:inherit;opacity:.5;animation:pulse 1.6s ease-out infinite}@keyframes pulse{0%{transform:scale(.6);opacity:.5}to{transform:scale(2.2);opacity:0}}.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:var(--fs-xs);background:var(--bg-2);color:var(--text-1);border:1px solid var(--hairline);white-space:nowrap}.pill.cyan{background:var(--accent-soft);color:var(--accent-hi);border-color:#48d7fe4d}.pill.good{background:#5fd37f24;color:var(--good);border-color:#5fd37f4d}.pill.warn{background:#f7b83d24;color:var(--warn);border-color:#f7b83d4d}.pill.bad{background:#fa686324;color:var(--bad);border-color:#fa68634d}.pill.active{background:var(--accent-soft);color:var(--accent-hi);border-color:#48d7fe73;box-shadow:0 0 10px var(--accent-glow),inset 0 0 0 1px #48d7fe40;text-shadow:0 0 8px var(--accent-glow)}.pill.disabled{background:#f3821d24;color:#ff9b51;color:oklch(.78 .17 55);border-color:#f3821d66}.segmented{display:inline-flex;background:var(--bg-1);border:1px solid var(--hairline);border-radius:var(--radius-s);padding:3px;gap:2px}.segmented button{background:transparent;border:0;color:var(--text-2);padding:5px 10px;border-radius:6px;cursor:default;font-size:var(--fs-xs);font-weight:500;display:inline-flex;align-items:center;gap:6px;transition:background .12s,color .12s}.segmented button:hover{color:var(--text-0)}.segmented button.active{background:var(--bg-3);color:var(--accent-hi);box-shadow:inset 0 0 0 1px var(--hairline-strong)}.toolbar{display:flex;align-items:center;gap:10px;padding:12px var(--pad-card);border-bottom:1px solid var(--hairline)}.toolbar .spacer{flex:1}.tbar-sep{display:inline-block;width:1px;height:18px;background:var(--hairline);margin:0 4px;align-self:center;flex:none}.input.input-bad{border-color:#dd503f99;box-shadow:0 0 0 3px #dd503f1f}.custom-toast{position:fixed;left:50%;bottom:28px;transform:translate(-50%);z-index:95;display:inline-flex;align-items:center;gap:10px;padding:10px 12px 10px 14px;background:var(--bg-1);border:1px solid var(--hairline-strong);border-radius:999px;box-shadow:0 12px 36px #00000059,0 0 0 1px #48d7fe40 inset,0 0 32px var(--accent-glow);color:var(--text-0);font-size:var(--fs-sm);animation:custom-toast-in .26s cubic-bezier(.2,.7,.2,1)}.custom-toast>svg{color:var(--accent-hi);flex:none}.custom-toast-x{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;border:0;background:var(--bg-3);color:var(--text-2);cursor:pointer;margin-left:2px}.custom-toast-x:hover{background:var(--bg-0);color:var(--text-0)}@keyframes custom-toast-in{0%{transform:translate(-50%,12px);opacity:0}to{transform:translate(-50%);opacity:1}}.tbl{width:100%;border-collapse:collapse}.tbl thead th{text-align:left;font-weight:500;font-size:var(--fs-xs);color:var(--text-2);padding:10px var(--pad-card);border-bottom:1px solid var(--hairline);background:var(--bg-1);position:sticky;top:0;letter-spacing:.02em;text-transform:uppercase}.tbl tbody tr{border-bottom:1px solid var(--hairline);transition:background .1s}.tbl tbody tr:hover{background:var(--bg-2)}.tbl tbody tr.selected{background:var(--accent-soft)}.tbl tbody td{padding:10px var(--pad-card);font-size:var(--fs-sm);height:var(--row-h);vertical-align:middle}.ch-logo{width:28px;height:28px;border-radius:6px;background:var(--bg-3);display:grid;place-items:center;font-size:10px;font-weight:700;color:var(--text-1);letter-spacing:.02em;flex:none;border:1px solid var(--hairline);overflow:hidden}.ch-logo.lg{width:56px;height:56px;font-size:14px;border-radius:10px}.ch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:12px;padding:var(--pad-card)}.ch-card{background:var(--bg-2);border:1px solid var(--hairline);border-radius:var(--radius-m);padding:14px;display:flex;flex-direction:column;gap:8px;cursor:default;transition:border-color .12s,background .12s,transform .12s;position:relative}.ch-card:hover{border-color:var(--hairline-strong);background:var(--bg-3)}.ch-card.selected{border-color:#48d7fe8c;box-shadow:0 0 0 1px #48d7fe4d,0 0 28px var(--accent-glow)}.ch-card .top{display:flex;align-items:flex-start;gap:10px}.ch-card .name{font-weight:600;font-size:var(--fs-sm);line-height:1.25}.ch-card .meta{color:var(--text-2);font-size:var(--fs-xs)}.ch-card .row{display:flex;align-items:center;justify-content:space-between;margin-top:auto}.ch-card .cbx-pos{position:absolute;top:10px;right:10px;opacity:0;transition:opacity .12s}.ch-card:hover .cbx-pos,.ch-card.selected .cbx-pos{opacity:1}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--gap);margin-bottom:var(--gap)}.stat{padding:var(--pad-card)}.stat .lbl{font-size:var(--fs-xs);color:var(--text-2);text-transform:uppercase;letter-spacing:.06em}.stat .val{font-size:28px;font-weight:600;letter-spacing:-.02em;margin-top:8px;font-variant-numeric:tabular-nums}.stat .delta{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-xs);margin-top:6px;color:var(--good)}.stat .delta.bad{color:var(--bad)}.src-row{display:grid;grid-template-columns:40px 1fr auto auto auto auto;align-items:center;gap:14px;padding:14px var(--pad-card);border-bottom:1px solid var(--hairline);transition:background .1s;cursor:default}.src-row:last-child{border-bottom:0}.src-row:hover{background:var(--bg-2)}.src-row .src-ico{width:40px;height:40px;border-radius:10px;background:var(--bg-3);display:grid;place-items:center;color:var(--accent);border:1px solid var(--hairline)}.src-row .src-name{font-weight:600;font-size:var(--fs-base);display:flex;align-items:center;gap:8px}.src-row .src-url{color:var(--text-2);font-size:var(--fs-xs);font-family:JetBrains Mono,ui-monospace,monospace;margin-top:3px}.src-row .stat-mini{font-size:var(--fs-xs);color:var(--text-2);text-align:right;min-width:60px}.src-row .stat-mini b{color:var(--text-0);font-size:var(--fs-base);font-weight:600;display:block}.epg{display:grid;grid-template-rows:36px 1fr;height:100%}.epg-head{display:grid;grid-template-columns:200px 1fr;border-bottom:1px solid var(--hairline-strong);position:sticky;top:0;z-index:4;background:var(--bg-1)}.epg-head .head-l{border-right:1px solid var(--hairline);display:flex;align-items:center;padding:0 14px;font-size:var(--fs-xs);color:var(--text-2);text-transform:uppercase;letter-spacing:.06em}.epg-head .head-r{display:flex;position:relative}.epg-time{flex:none;font-size:var(--fs-xs);color:var(--text-2);padding:0 8px;border-right:1px solid var(--hairline);display:flex;align-items:center}.epg-body{overflow:auto}.epg-row{display:grid;grid-template-columns:200px 1fr;border-bottom:1px solid var(--hairline);min-height:76px}.epg-row .ch{border-right:1px solid var(--hairline);display:flex;align-items:center;gap:10px;padding:0 14px;background:var(--bg-1);position:sticky;left:0;z-index:2}.epg-row .ch .nm{font-size:var(--fs-sm);font-weight:500}.epg-row .ch .num{color:var(--text-3);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}.epg-progs{position:relative}.epg-prog{position:absolute;top:8px;bottom:8px;background:var(--bg-2);border:1px solid var(--hairline);border-radius:8px;padding:8px 11px;font-size:var(--fs-xs);overflow:hidden;display:flex;flex-direction:column;justify-content:center;gap:3px;cursor:default;transition:background .1s,border-color .1s,transform .1s}.epg-prog:hover{background:var(--bg-3);border-color:var(--hairline-strong)}.epg-prog.live{background:var(--accent-soft);border-color:#48d7fe66;color:var(--accent-hi)}.epg-prog .t{font-weight:600;color:var(--text-0);line-height:1.25}.epg-prog.live .t{color:var(--accent-hi)}.epg-prog .sub{color:var(--text-2);font-size:10.5px;line-height:1.35}.now-line{position:absolute;top:0;bottom:0;width:2px;background:var(--accent);box-shadow:0 0 12px var(--accent);z-index:3;pointer-events:none}.now-line:before{content:"";position:absolute;top:-3px;left:-4px;width:10px;height:10px;background:var(--accent);border-radius:50%;box-shadow:0 0 12px var(--accent)}.dropzone{border:2px dashed var(--hairline-strong);border-radius:var(--radius-l);padding:56px 24px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;background:var(--bg-1);cursor:default;transition:border-color .15s,background .15s}.dropzone:hover,.dropzone.over{border-color:var(--accent);background:var(--accent-soft)}.dropzone .icon-circle{width:56px;height:56px;border-radius:50%;background:var(--accent-soft);display:grid;place-items:center;color:var(--accent)}.dropzone h3{margin:0;font-size:17px;font-weight:600;letter-spacing:-.01em}.dropzone p{margin:0;color:var(--text-2);font-size:var(--fs-sm)}.player{background:#000;border:1px solid var(--hairline);border-radius:var(--radius-m);aspect-ratio:16 / 9;position:relative;overflow:hidden}.player .stripes{position:absolute;top:0;right:0;bottom:0;left:0;background:repeating-linear-gradient(45deg,#171b1f,#171b1f 12px,#0e1216 12px,#0e1216 24px);opacity:.5}.player .label{position:absolute;left:12px;top:10px;font-family:JetBrains Mono,ui-monospace,monospace;font-size:11px;color:var(--text-2);background:#00000080;padding:3px 8px;border-radius:6px;border:1px solid var(--hairline)}.player .play{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center}.player .play-btn{width:64px;height:64px;border-radius:50%;background:var(--accent);display:grid;place-items:center;box-shadow:0 0 32px var(--accent-glow);color:#001322;color:oklch(.18 .05 240)}.player .controls{position:absolute;bottom:0;left:0;right:0;height:44px;background:linear-gradient(180deg,transparent,rgba(0,0,0,.7));display:flex;align-items:center;padding:0 14px;gap:10px;color:var(--text-1)}.player .controls .track{flex:1;height:3px;background:var(--hairline-strong);border-radius:999px;position:relative}.player .controls .track:after{content:"";position:absolute;left:0;top:0;bottom:0;width:32%;background:var(--accent);border-radius:999px;box-shadow:0 0 12px var(--accent)}.section-title{font-size:var(--fs-h2);font-weight:600;margin:0 0 var(--gap);letter-spacing:-.01em;display:flex;align-items:center;gap:10px}.map-grid{display:grid;grid-template-columns:1fr 40px 1fr;gap:14px;align-items:stretch}.map-col{background:var(--bg-1);border:1px solid var(--hairline);border-radius:var(--radius-m);overflow:hidden;display:flex;flex-direction:column;min-height:480px}.map-col h3{margin:0;font-size:var(--fs-sm);font-weight:600;padding:12px 14px;border-bottom:1px solid var(--hairline);color:var(--text-1);display:flex;align-items:center;gap:8px}.map-list{overflow-y:auto;flex:1}.map-item{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--hairline);cursor:default;transition:background .1s}.map-item:hover{background:var(--bg-2)}.map-item.selected{background:var(--accent-soft)}.map-item.matched{opacity:.55}.map-item .nm{font-size:var(--fs-sm);font-weight:500;flex:1}.map-item .id{font-size:var(--fs-xs);color:var(--text-2);font-family:JetBrains Mono,ui-monospace,monospace}.map-link{display:grid;place-items:center;color:var(--text-3)}.match-row{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--hairline);font-size:var(--fs-sm)}.match-row .arr{color:var(--accent)}.act{display:flex;gap:12px;padding:12px var(--pad-card);border-bottom:1px solid var(--hairline);font-size:var(--fs-sm)}.act:last-child{border-bottom:0}.act .when{color:var(--text-3);font-size:var(--fs-xs);margin-top:2px;font-variant-numeric:tabular-nums}.act .ico-w{width:28px;height:28px;border-radius:50%;background:var(--bg-2);border:1px solid var(--hairline);display:grid;place-items:center;color:var(--text-1);flex:none}.act b{font-weight:600}.sync-grid{display:flex;flex-direction:column;margin-top:4px}.sync-row{display:grid;grid-template-columns:28px 1fr 180px 130px 44px;align-items:center;column-gap:14px;padding:12px 0;border-bottom:1px solid var(--hairline);transition:background .1s,opacity .12s}.sync-row.sync-row-last,.sync-row:last-child{border-bottom:0}.sync-row.disabled{opacity:.5}.sync-row-lvl .sched-ico{display:none}.sync-row-lvl .sync-meta{grid-column:1 / span 2}.sync-row-sched:hover{background:var(--bg-2);margin:0 -10px;padding-left:10px;padding-right:10px}.sync-meta{min-width:0}.sync-lbl{font-size:var(--fs-base);font-weight:500;color:var(--text-0)}.sync-hint{font-size:var(--fs-xs);color:var(--text-2);margin-top:3px}.sync-col-sched,.sync-col-cron,.sync-col-toggle{display:flex;align-items:center}.sync-col-toggle,.sync-col-cron{justify-content:flex-end}.sync-section-hd{display:flex;align-items:center;gap:8px;padding:14px 0 10px;font-size:var(--fs-sm);font-weight:600;color:var(--text-1);border-bottom:1px solid var(--hairline);margin-top:4px}.sync-section-hd>.ico:first-child{color:var(--text-1)}.sync-section-hd.collapsible{cursor:default;-webkit-user-select:none;user-select:none}.sync-section-hd.collapsible:hover{color:var(--text-0)}.sync-section-hd .chev{color:var(--text-2);transition:transform .18s ease}.sync-section-hd.collapsible.open .chev{transform:rotate(90deg)}.endpoint-field{display:grid;grid-template-columns:160px 1fr auto;align-items:center;gap:10px}.endpoint-lbl{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);font-weight:500;color:var(--text-1)}.endpoint-ico{width:26px;height:26px;border-radius:7px;background:var(--bg-2);border:1px solid var(--hairline);display:grid;place-items:center}.sched-ico{width:28px;height:28px;border-radius:7px;background:var(--bg-3);border:1px solid var(--hairline);display:grid;place-items:center;color:var(--text-1)}.sched-ico.is-epg{color:var(--good)}.sched-ico.builtin{background:linear-gradient(135deg,#012e3a,#102238,#161635);color:var(--accent-hi);border-color:#48d7fe4d}.sched-ico.builtin.epg-builtin{background:linear-gradient(135deg,#023310,#00220b);background:linear-gradient(135deg,#023310,color(xyz 0.005 0.012 0.003));color:var(--good);border-color:#5fd37f59}.cron-chip{font-family:JetBrains Mono,ui-monospace,monospace;font-size:10.5px;padding:4px 8px;border-radius:6px;background:var(--bg-1);border:1px solid var(--hairline);color:var(--accent-hi);cursor:default;letter-spacing:.02em;transition:border-color .12s,background .12s;white-space:nowrap}.cron-chip:hover{border-color:var(--accent);background:var(--accent-soft)}.sync-col-sched .select:after{display:none}.sync-col-sched .select select{padding-right:12px;width:100%}.sidebar-foot-stack{margin-top:auto;display:flex;flex-direction:column;gap:8px}.logs-btn{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--radius-s);background:var(--bg-1);border:1px solid var(--hairline);color:var(--text-1);font-size:var(--fs-sm);cursor:default;transition:background .12s,border-color .12s,color .12s}.logs-btn:hover{background:var(--bg-2);border-color:var(--hairline-strong);color:var(--text-0)}.logs-btn-ico{position:relative;width:22px;height:22px;border-radius:6px;background:var(--bg-2);border:1px solid var(--hairline);display:grid;place-items:center;color:var(--text-1)}.logs-btn-ico .dot{position:absolute;top:-1px;right:-1px}.logs-drawer-wrap{position:fixed;top:0;right:0;bottom:0;left:0;z-index:88;pointer-events:none}.logs-drawer-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:auto;animation:fade-in .18s ease-out}.logs-drawer{position:absolute;left:0;right:0;bottom:0;height:46vh;min-height:280px;pointer-events:auto;display:flex;flex-direction:column;border-bottom:0;border-left:0;border-right:0;animation:slidein-bottom .26s cubic-bezier(.2,.7,.2,1)}@keyframes slidein-bottom{0%{transform:translateY(30px);opacity:0}to{transform:none;opacity:1}}.logs-hd{flex:none;display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid var(--hairline);background:linear-gradient(180deg,#0000002e,#0000)}[data-theme=light] .logs-hd{background:linear-gradient(180deg,#fff6,#fff0)}.logs-hd .row{flex:1}.logs-body{flex:1;overflow-y:auto;padding:8px 0;font-family:JetBrains Mono,ui-monospace,monospace;font-size:11.5px;line-height:1.55}.log-line{display:grid;grid-template-columns:90px 60px 60px 1fr;gap:12px;padding:3px 16px;color:var(--text-1);border-left:2px solid transparent}.log-line:hover{background:#ffffff06}[data-theme=light] .log-line:hover{background:#00000008}.log-ts{color:var(--text-3)}.log-lvl{font-weight:600;font-size:10.5px;letter-spacing:.04em;padding-top:1px}.log-src{color:var(--text-2)}.log-msg{color:var(--text-1);word-break:break-word}.log-line.log-warn{border-left-color:#f7b83d80;background:#f7b83d0a}.log-line.log-error{border-left-color:#fa686399;background:#fa68630f}.log-line.log-error .log-msg{color:#ffbeb8;color:oklch(.86 .12 25)}.log-line.log-ok{border-left-color:#5fd37f66}.log-line.log-debug{opacity:.7}.hist-row{display:flex;align-items:flex-start;gap:14px;padding:12px var(--pad-card);border-bottom:1px solid var(--hairline)}.hist-row:last-child{border-bottom:0}.hist-row:hover{background:var(--bg-2)}.hist-ico{width:30px;height:30px;border-radius:8px;background:var(--bg-2);border:1px solid var(--hairline);display:grid;place-items:center;flex:none;margin-top:2px}.hm-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:14px;min-height:0}.hm-list{overflow:hidden}.hm-detail{overflow-y:auto;max-height:720px}.qoe-pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px 2px 7px;border-radius:999px;font-size:11.5px;font-weight:600;font-family:JetBrains Mono,ui-monospace,monospace;border:1px solid var(--hairline)}.qoe-pill[data-health=good]{background:#5fd37f24;color:var(--good);border-color:#5fd37f4d}.qoe-pill[data-health=good] .dot{background:var(--good);box-shadow:0 0 6px var(--good)}.qoe-pill[data-health=warn]{background:#f7b83d24;color:var(--warn);border-color:#f7b83d4d}.qoe-pill[data-health=warn] .dot{background:var(--warn);box-shadow:0 0 6px var(--warn)}.qoe-pill[data-health=bad]{background:#fa686324;color:var(--bad);border-color:#fa68634d}.qoe-pill[data-health=bad] .dot{background:var(--bad);box-shadow:0 0 6px var(--bad)}.qoe-pill .dot{width:6px;height:6px;border-radius:50%}.buf-bars{display:flex;align-items:flex-end;gap:3px;height:110px;padding:4px 0}.buf-bar-wrap{flex:1;height:100%;display:flex;align-items:flex-end}.buf-bar{width:100%;border-radius:3px 3px 0 0;min-height:2px}.buf-timeline{position:relative;height:32px;background:var(--bg-2);border-radius:6px;border:1px solid var(--hairline);overflow:hidden}.buf-timeline-empty{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;gap:6px;color:var(--good);font-size:11px}.buf-event{position:absolute;top:4px;bottom:4px;background:var(--bad);border-radius:3px;box-shadow:0 0 8px #fa686399;cursor:default}.logs-scroll-btn{position:absolute;left:50%;bottom:14px;transform:translate(-50%);background:var(--accent);color:#001322;color:oklch(.18 .05 240);border:0;padding:6px 12px;border-radius:999px;font-size:11px;font-weight:600;cursor:default;box-shadow:0 4px 18px var(--accent-glow),0 0 0 1px #bef6 inset;box-shadow:0 4px 18px var(--accent-glow),0 0 0 1px oklch(.92 .1 220 / .4) inset;display:inline-flex;align-items:center;gap:6px;z-index:5}.drawer-wrap{position:fixed;top:0;right:0;bottom:0;left:0;z-index:80;pointer-events:none}.drawer-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:auto;animation:fade-in .2s ease-out}.drawer-panel{position:absolute;top:0;right:0;bottom:0;width:440px;pointer-events:auto;display:flex;flex-direction:column;border-radius:0;border-right:0;border-top:0;border-bottom:0;animation:slidein-right .26s cubic-bezier(.2,.7,.2,1)}.drawer-hd{padding:16px 22px;border-bottom:1px solid var(--hairline);display:flex;align-items:center;gap:12px;background:linear-gradient(180deg,#00000026,#0000);flex:none}[data-theme=light] .drawer-hd{background:linear-gradient(180deg,#fff6,#fff0)}.drawer-body{padding:22px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;flex:1}.glass{background:linear-gradient(180deg,#ffffff0a,#fff0 40%),#141619b8;backdrop-filter:blur(28px) saturate(140%);-webkit-backdrop-filter:blur(28px) saturate(140%);border:1px solid oklch(1 0 0 / .08);box-shadow:inset 0 1px #ffffff0f,inset 0 0 0 1px #48d7fe0d,-1px 0 #48d7fe14,0 30px 80px #00000080,0 0 80px #48d7fe1a}[data-theme=light] .glass{background:linear-gradient(180deg,#ffffffb3,#ffffff80 40%),#f3f5f7b3;border-color:#00000014;box-shadow:inset 0 1px #fff9,0 30px 80px #0624372e,0 0 80px #48d7fe1f}.glass-bg{background:#02030473;backdrop-filter:blur(8px) saturate(120%);-webkit-backdrop-filter:blur(8px) saturate(120%)}[data-theme=light] .glass-bg{background:#7b818652}.modal-bg{position:fixed;top:0;right:0;bottom:0;left:0;background:#0203048c;backdrop-filter:blur(10px) saturate(120%);-webkit-backdrop-filter:blur(10px) saturate(120%);z-index:90;display:grid;place-items:center;animation:fade-in .18s ease-out}[data-theme=light] .modal-bg{background:#7b81865c}.modal{width:560px;background:linear-gradient(180deg,#ffffff0a,#fff0 40%),#141619c7;backdrop-filter:blur(32px) saturate(140%);-webkit-backdrop-filter:blur(32px) saturate(140%);border:1px solid oklch(1 0 0 / .1);border-radius:var(--radius-l);box-shadow:inset 0 1px #ffffff12,0 30px 80px #0000008c,0 0 100px #48d7fe2e;overflow:hidden;animation:modal-in .24s cubic-bezier(.2,.7,.2,1)}[data-theme=light] .modal{background:linear-gradient(180deg,#ffffffbf,#ffffff8c 40%),#f3f5f7b3;border-color:#00000014;box-shadow:inset 0 1px #ffffffb3,0 30px 80px #06243738,0 0 100px #48d7fe33}.modal-hd{padding:18px 22px;border-bottom:1px solid var(--hairline);display:flex;align-items:center;gap:10px;background:linear-gradient(180deg,oklch(1 0 0 / .02),transparent)}.modal-hd h2{margin:0;font-size:16px;font-weight:600}.modal-body{padding:22px;display:flex;flex-direction:column;gap:14px}.modal-ft{padding:14px 22px;border-top:1px solid var(--hairline);display:flex;justify-content:flex-end;gap:8px;background:#0000002e}[data-theme=light] .modal-ft{background:#00000008}@keyframes modal-in{0%{transform:translateY(12px) scale(.98);opacity:0}to{transform:none;opacity:1}}.field-lbl{font-size:var(--fs-xs);color:var(--text-2);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}.form-row{display:flex;flex-direction:column}.form-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}.empty{padding:60px 20px;text-align:center;color:var(--text-2)}.empty h3{color:var(--text-0);margin:0 0 6px;font-size:16px;font-weight:600}[data-theme=dark] .twk-panel{background:#1c1e24d9!important;color:var(--text-0)!important;border-color:#ffffff14!important;box-shadow:0 12px 40px #00000080,0 0 60px #48d7fe0d!important}[data-theme=dark] .twk-panel *{color:inherit}[data-theme=dark] .twk-lbl{color:var(--text-1)!important}[data-theme=dark] .twk-val{color:var(--text-3)!important}.streams-grid{display:grid;grid-template-columns:380px 1fr;gap:14px;height:100%;min-height:0}.streams-list{display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--hairline);border-radius:var(--radius-m);overflow:hidden;min-height:0}.streams-list .body{overflow-y:auto;flex:1}.stream-item{padding:12px 14px;display:grid;grid-template-columns:36px 1fr auto;align-items:center;gap:12px;border-bottom:1px solid var(--hairline);cursor:default;transition:background .1s;border-left:2px solid transparent}.stream-item:hover{background:var(--bg-2)}.stream-item.selected{background:var(--accent-soft);border-left-color:var(--accent)}.stream-item .nm{font-weight:500;font-size:var(--fs-sm);display:flex;align-items:center;gap:8px}.stream-item .meta{font-size:var(--fs-xs);color:var(--text-2);margin-top:3px;display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}.stream-item .meta .mono{color:var(--text-1)}.stream-item .viewer{text-align:right;font-variant-numeric:tabular-nums}.stream-item .viewer b{font-size:var(--fs-base);font-weight:600;color:var(--text-0);display:block}.stream-item .viewer span{font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:.06em}.stream-detail{background:var(--bg-1);border:1px solid var(--hairline);border-radius:var(--radius-m);overflow-y:auto;min-height:0}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.metric{padding:12px 14px;background:var(--bg-2);border:1px solid var(--hairline);border-radius:10px}.metric .lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.06em}.metric .val{font-size:19px;font-weight:600;letter-spacing:-.01em;margin-top:6px;font-variant-numeric:tabular-nums}.metric .sub{font-size:var(--fs-xs);color:var(--text-3);margin-top:2px}.kv-list{display:grid;grid-template-columns:130px 1fr;row-gap:8px;column-gap:14px;font-size:var(--fs-sm)}.kv-list .k{color:var(--text-2)}.kv-list .v{color:var(--text-0)}.live-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px 3px 8px;border-radius:999px;background:#fa686326;color:#ff958d;color:oklch(.78 .18 25);border:1px solid oklch(.7 .18 25 / .35);font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}.live-pill .dot{background:#fa6863;box-shadow:0 0 8px #fa6863}.stream-view-bg{position:fixed;top:0;right:0;bottom:0;left:0;z-index:85;background:#02030473;backdrop-filter:blur(8px) saturate(120%);-webkit-backdrop-filter:blur(8px) saturate(120%);animation:fade-in .2s ease-out}[data-theme=light] .stream-view-bg{background:#7b818652}.stream-view{position:absolute;top:0;right:0;bottom:0;width:50vw;min-width:560px;background:linear-gradient(180deg,#ffffff0a,#fff0 40%),#141619bd;backdrop-filter:blur(30px) saturate(140%);-webkit-backdrop-filter:blur(30px) saturate(140%);border-left:1px solid oklch(1 0 0 / .1);box-shadow:inset 1px 0 #ffffff0f,-1px 0 #48d7fe2e,-30px 0 80px #0000008c,-40px 0 120px #48d7fe1f;display:flex;flex-direction:column;animation:slidein-right .26s cubic-bezier(.2,.7,.2,1)}[data-theme=light] .stream-view{background:linear-gradient(180deg,#ffffffbf,#ffffff8c 40%),#f3f5f7b3;border-left-color:#00000014;box-shadow:inset 1px 0 #ffffffb3,-1px 0 #48d7fe38,-30px 0 80px #06243738,-40px 0 120px #48d7fe26}.stream-view-hd{display:flex;align-items:center;gap:12px;padding:16px 22px;border-bottom:1px solid var(--hairline);background:linear-gradient(180deg,#00000026,#0000);flex:none}[data-theme=light] .stream-view-hd{background:linear-gradient(180deg,#fff6,#fff0)}.stream-view-body{flex:1;overflow-y:auto;padding:18px 22px 28px;display:flex;flex-direction:column;gap:14px}@keyframes slidein-right{0%{transform:translate(40px);opacity:0}to{transform:none;opacity:1}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.player-ctrl{background:transparent;border:0;color:var(--text-1);width:28px;height:28px;border-radius:6px;display:grid;place-items:center;cursor:default;transition:background .12s,color .12s}.player-ctrl:hover{background:#ffffff14;color:#fff}.pill.system{background:linear-gradient(135deg,#48d7fe38,#6f6de838);color:var(--accent-hi);border-color:#48d7fe66;font-weight:600;letter-spacing:.04em}.src-row .src-ico.builtin{background:linear-gradient(135deg,#012e3a,#102238,#161635);color:var(--accent-hi);border-color:#48d7fe4d;box-shadow:inset 0 0 0 1px #48d7fe26,0 0 18px #48d7fe26}.src-row .src-ico.builtin.epg-builtin,.src-ico.builtin.epg-builtin{background:linear-gradient(135deg,#023310,#00220b);background:linear-gradient(135deg,#023310,color(xyz 0.005 0.012 0.003));color:var(--good);border-color:#5fd37f59;box-shadow:inset 0 0 0 1px #5fd37f33,0 0 18px #5fd37f33}.row{display:flex;align-items:center;gap:10px}.col{display:flex;flex-direction:column;gap:10px}.muted{color:var(--text-2)}.mono{font-family:JetBrains Mono,ui-monospace,monospace}.divider{height:1px;background:var(--hairline);margin:8px 0}.spacer{flex:1} diff --git a/dist/assets/index-CQPQcDLN.js b/dist/assets/index-CQPQcDLN.js new file mode 100644 index 00000000..d17a8f29 --- /dev/null +++ b/dist/assets/index-CQPQcDLN.js @@ -0,0 +1,26 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/PlaylistsScreen-0ooKY6SX.js","assets/useSettings-CPUgOpin.js","assets/PlaylistDetailScreen-F92VSAQ7.js","assets/Stat.vue_vue_type_script_setup_true_lang-BLQk8QX-.js","assets/EPGDetailScreen-CX4y1Ve9.js","assets/MappingScreen-BdiMBcth.js","assets/SettingsScreen-Daj_a2gr.js"])))=>i.map(i=>d[i]); +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function s(i){if(i.ep)return;i.ep=!0;const r=n(i);fetch(i.href,r)}})();/** +* @vue/shared v3.5.35 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function qs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const ae={},zt=[],ft=()=>{},dr=()=>!1,Yn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Jn=e=>e.startsWith("onUpdate:"),Ce=Object.assign,Ys=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Vo=Object.prototype.hasOwnProperty,se=(e,t)=>Vo.call(e,t),G=Array.isArray,Wt=e=>An(e)==="[object Map]",Qn=e=>An(e)==="[object Set]",mi=e=>An(e)==="[object Date]",W=e=>typeof e=="function",ce=e=>typeof e=="string",Je=e=>typeof e=="symbol",ie=e=>e!==null&&typeof e=="object",pr=e=>(ie(e)||W(e))&&W(e.then)&&W(e.catch),hr=Object.prototype.toString,An=e=>hr.call(e),$o=e=>An(e).slice(8,-1),mr=e=>An(e)==="[object Object]",Js=e=>ce(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,fn=qs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Xn=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Fo=/-\w/g,Ne=Xn(e=>e.replace(Fo,t=>t.slice(1).toUpperCase())),Bo=/\B([A-Z])/g,Bt=Xn(e=>e.replace(Bo,"-$1").toLowerCase()),Zn=Xn(e=>e.charAt(0).toUpperCase()+e.slice(1)),hs=Xn(e=>e?`on${Zn(e)}`:""),ct=(e,t)=>!Object.is(e,t),Dn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},es=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let gi;const ts=()=>gi||(gi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function dt(e){if(G(e)){const t={};for(let n=0;n{if(n){const s=n.split(jo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function Be(e){let t="";if(ce(e))t=e;else if(G(e))for(let n=0;nRn(n,t))}const yr=e=>!!(e&&e.__v_isRef===!0),J=e=>ce(e)?e:e==null?"":G(e)||ie(e)&&(e.toString===hr||!W(e.toString))?yr(e)?J(e.value):JSON.stringify(e,br,2):String(e),br=(e,t)=>yr(t)?br(e,t.value):Wt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,i],r)=>(n[ms(s,r)+" =>"]=i,n),{})}:Qn(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>ms(n))}:Je(t)?ms(t):ie(t)&&!G(t)&&!mr(t)?String(t):t,ms=(e,t="")=>{var n;return Je(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.35 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ee;class Jo{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Ee&&(Ee.active?(this.parent=Ee,this.index=(Ee.scopes||(Ee.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0){if(Ee===this)Ee=this.prevScope;else{let t=Ee;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(pn){let t=pn;for(pn=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;dn;){let t=dn;for(dn=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Er(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Sr(e){let t,n=e.depsTail,s=n;for(;s;){const i=s.prevDep;s.version===-1?(s===n&&(n=i),Zs(s),Xo(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=i}e.deps=t,e.depsTail=n}function Os(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Cr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Cr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===bn)||(e.globalVersion=bn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Os(e))))return;e.flags|=2;const t=e.dep,n=ue,s=qe;ue=e,qe=!0;try{Er(e);const i=e.fn(e._value);(t.version===0||ct(i,e._value))&&(e.flags|=128,e._value=i,t.version++)}catch(i){throw t.version++,i}finally{ue=n,qe=s,Sr(e),e.flags&=-3}}function Zs(e,t=!1){const{dep:n,prevSub:s,nextSub:i}=e;if(s&&(s.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let r=n.computed.deps;r;r=r.nextDep)Zs(r,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Xo(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let qe=!0;const Ar=[];function bt(){Ar.push(qe),qe=!1}function _t(){const e=Ar.pop();qe=e===void 0?!0:e}function vi(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ue;ue=void 0;try{t()}finally{ue=n}}}let bn=0;class Zo{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class ei{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ue||!qe||ue===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ue)n=this.activeLink=new Zo(ue,this),ue.deps?(n.prevDep=ue.depsTail,ue.depsTail.nextDep=n,ue.depsTail=n):ue.deps=ue.depsTail=n,Rr(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ue.depsTail,n.nextDep=void 0,ue.depsTail.nextDep=n,ue.depsTail=n,ue.deps===n&&(ue.deps=s)}return n}trigger(t){this.version++,bn++,this.notify(t)}notify(t){Qs();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Xs()}}}function Rr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Rr(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Is=new WeakMap,Vt=Symbol(""),Ns=Symbol(""),_n=Symbol("");function Re(e,t,n){if(qe&&ue){let s=Is.get(e);s||Is.set(e,s=new Map);let i=s.get(n);i||(s.set(n,i=new ei),i.map=s,i.key=n),i.track()}}function vt(e,t,n,s,i,r){const o=Is.get(e);if(!o){bn++;return}const a=l=>{l&&l.trigger()};if(Qs(),t==="clear")o.forEach(a);else{const l=G(e),u=l&&Js(n);if(l&&n==="length"){const c=Number(s);o.forEach((p,g)=>{(g==="length"||g===_n||!Je(g)&&g>=c)&&a(p)})}else switch((n!==void 0||o.has(void 0))&&a(o.get(n)),u&&a(o.get(_n)),t){case"add":l?u&&a(o.get("length")):(a(o.get(Vt)),Wt(e)&&a(o.get(Ns)));break;case"delete":l||(a(o.get(Vt)),Wt(e)&&a(o.get(Ns)));break;case"set":Wt(e)&&a(o.get(Vt));break}}Xs()}function Ut(e){const t=ne(e);return t===e?t:(Re(t,"iterate",_n),Ke(e)?t:t.map(Qe))}function ns(e){return Re(e=ne(e),"iterate",_n),e}function at(e,t){return wt(e)?Zt($t(e)?Qe(t):t):Qe(t)}const el={__proto__:null,[Symbol.iterator](){return vs(this,Symbol.iterator,e=>at(this,e))},concat(...e){return Ut(this).concat(...e.map(t=>G(t)?Ut(t):t))},entries(){return vs(this,"entries",e=>(e[1]=at(this,e[1]),e))},every(e,t){return pt(this,"every",e,t,void 0,arguments)},filter(e,t){return pt(this,"filter",e,t,n=>n.map(s=>at(this,s)),arguments)},find(e,t){return pt(this,"find",e,t,n=>at(this,n),arguments)},findIndex(e,t){return pt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return pt(this,"findLast",e,t,n=>at(this,n),arguments)},findLastIndex(e,t){return pt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return pt(this,"forEach",e,t,void 0,arguments)},includes(...e){return ys(this,"includes",e)},indexOf(...e){return ys(this,"indexOf",e)},join(e){return Ut(this).join(e)},lastIndexOf(...e){return ys(this,"lastIndexOf",e)},map(e,t){return pt(this,"map",e,t,void 0,arguments)},pop(){return rn(this,"pop")},push(...e){return rn(this,"push",e)},reduce(e,...t){return yi(this,"reduce",e,t)},reduceRight(e,...t){return yi(this,"reduceRight",e,t)},shift(){return rn(this,"shift")},some(e,t){return pt(this,"some",e,t,void 0,arguments)},splice(...e){return rn(this,"splice",e)},toReversed(){return Ut(this).toReversed()},toSorted(e){return Ut(this).toSorted(e)},toSpliced(...e){return Ut(this).toSpliced(...e)},unshift(...e){return rn(this,"unshift",e)},values(){return vs(this,"values",e=>at(this,e))}};function vs(e,t,n){const s=ns(e),i=s[t]();return s!==e&&!Ke(e)&&(i._next=i.next,i.next=()=>{const r=i._next();return r.done||(r.value=n(r.value)),r}),i}const tl=Array.prototype;function pt(e,t,n,s,i,r){const o=ns(e),a=o!==e&&!Ke(e),l=o[t];if(l!==tl[t]){const p=l.apply(e,r);return a?Qe(p):p}let u=n;o!==e&&(a?u=function(p,g){return n.call(this,at(e,p),g,e)}:n.length>2&&(u=function(p,g){return n.call(this,p,g,e)}));const c=l.call(o,u,s);return a&&i?i(c):c}function yi(e,t,n,s){const i=ns(e),r=i!==e&&!Ke(e);let o=n,a=!1;i!==e&&(r?(a=s.length===0,o=function(u,c,p){return a&&(a=!1,u=at(e,u)),n.call(this,u,at(e,c),p,e)}):n.length>3&&(o=function(u,c,p){return n.call(this,u,c,p,e)}));const l=i[t](o,...s);return a?at(e,l):l}function ys(e,t,n){const s=ne(e);Re(s,"iterate",_n);const i=s[t](...n);return(i===-1||i===!1)&&si(n[0])?(n[0]=ne(n[0]),s[t](...n)):i}function rn(e,t,n=[]){bt(),Qs();const s=ne(e)[t].apply(e,n);return Xs(),_t(),s}const nl=qs("__proto__,__v_isRef,__isVue"),Mr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Je));function sl(e){Je(e)||(e=String(e));const t=ne(this);return Re(t,"has",e),t.hasOwnProperty(e)}class Tr{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const i=this._isReadonly,r=this._isShallow;if(n==="__v_isReactive")return!i;if(n==="__v_isReadonly")return i;if(n==="__v_isShallow")return r;if(n==="__v_raw")return s===(i?r?pl:Ir:r?Or:kr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=G(t);if(!i){let l;if(o&&(l=el[n]))return l;if(n==="hasOwnProperty")return sl}const a=Reflect.get(t,n,Te(t)?t:s);if((Je(n)?Mr.has(n):nl(n))||(i||Re(t,"get",n),r))return a;if(Te(a)){const l=o&&Js(n)?a:a.value;return i&&ie(l)?Ds(l):l}return ie(a)?i?Ds(a):Mn(a):a}}class Pr extends Tr{constructor(t=!1){super(!1,t)}set(t,n,s,i){let r=t[n];const o=G(t)&&Js(n);if(!this._isShallow){const u=wt(r);if(!Ke(s)&&!wt(s)&&(r=ne(r),s=ne(s)),!o&&Te(r)&&!Te(s))return u||(r.value=s),!0}const a=o?Number(n)e,kn=e=>Reflect.getPrototypeOf(e);function al(e,t,n){return function(...s){const i=this.__v_raw,r=ne(i),o=Wt(r),a=e==="entries"||e===Symbol.iterator&&o,l=e==="keys"&&o,u=i[e](...s),c=n?Ls:t?Zt:Qe;return!t&&Re(r,"iterate",l?Ns:Vt),Ce(Object.create(u),{next(){const{value:p,done:g}=u.next();return g?{value:p,done:g}:{value:a?[c(p[0]),c(p[1])]:c(p),done:g}}})}}function On(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function ul(e,t){const n={get(i){const r=this.__v_raw,o=ne(r),a=ne(i);e||(ct(i,a)&&Re(o,"get",i),Re(o,"get",a));const{has:l}=kn(o),u=t?Ls:e?Zt:Qe;if(l.call(o,i))return u(r.get(i));if(l.call(o,a))return u(r.get(a));r!==o&&r.get(i)},get size(){const i=this.__v_raw;return!e&&Re(ne(i),"iterate",Vt),i.size},has(i){const r=this.__v_raw,o=ne(r),a=ne(i);return e||(ct(i,a)&&Re(o,"has",i),Re(o,"has",a)),i===a?r.has(i):r.has(i)||r.has(a)},forEach(i,r){const o=this,a=o.__v_raw,l=ne(a),u=t?Ls:e?Zt:Qe;return!e&&Re(l,"iterate",Vt),a.forEach((c,p)=>i.call(r,u(c),u(p),o))}};return Ce(n,e?{add:On("add"),set:On("set"),delete:On("delete"),clear:On("clear")}:{add(i){const r=ne(this),o=kn(r),a=ne(i),l=!t&&!Ke(i)&&!wt(i)?a:i;return o.has.call(r,l)||ct(i,l)&&o.has.call(r,i)||ct(a,l)&&o.has.call(r,a)||(r.add(l),vt(r,"add",l,l)),this},set(i,r){!t&&!Ke(r)&&!wt(r)&&(r=ne(r));const o=ne(this),{has:a,get:l}=kn(o);let u=a.call(o,i);u||(i=ne(i),u=a.call(o,i));const c=l.call(o,i);return o.set(i,r),u?ct(r,c)&&vt(o,"set",i,r):vt(o,"add",i,r),this},delete(i){const r=ne(this),{has:o,get:a}=kn(r);let l=o.call(r,i);l||(i=ne(i),l=o.call(r,i)),a&&a.call(r,i);const u=r.delete(i);return l&&vt(r,"delete",i,void 0),u},clear(){const i=ne(this),r=i.size!==0,o=i.clear();return r&&vt(i,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(i=>{n[i]=al(i,e,t)}),n}function ti(e,t){const n=ul(e,t);return(s,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?s:Reflect.get(se(n,i)&&i in s?n:s,i,r)}const cl={get:ti(!1,!1)},fl={get:ti(!1,!0)},dl={get:ti(!0,!1)};const kr=new WeakMap,Or=new WeakMap,Ir=new WeakMap,pl=new WeakMap;function hl(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Mn(e){return wt(e)?e:ni(e,!1,rl,cl,kr)}function Nr(e){return ni(e,!1,ll,fl,Or)}function Ds(e){return ni(e,!0,ol,dl,Ir)}function ni(e,t,n,s,i){if(!ie(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const r=i.get(e);if(r)return r;const o=hl($o(e));if(o===0)return e;const a=new Proxy(e,o===2?s:n);return i.set(e,a),a}function $t(e){return wt(e)?$t(e.__v_raw):!!(e&&e.__v_isReactive)}function wt(e){return!!(e&&e.__v_isReadonly)}function Ke(e){return!!(e&&e.__v_isShallow)}function si(e){return e?!!e.__v_raw:!1}function ne(e){const t=e&&e.__v_raw;return t?ne(t):e}function ml(e){return!se(e,"__v_skip")&&Object.isExtensible(e)&&gr(e,"__v_skip",!0),e}const Qe=e=>ie(e)?Mn(e):e,Zt=e=>ie(e)?Ds(e):e;function Te(e){return e?e.__v_isRef===!0:!1}function Oe(e){return Lr(e,!1)}function gl(e){return Lr(e,!0)}function Lr(e,t){return Te(e)?e:new vl(e,t)}class vl{constructor(t,n){this.dep=new ei,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:ne(t),this._value=n?t:Qe(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ke(t)||wt(t);t=s?t:ne(t),ct(t,n)&&(this._rawValue=t,this._value=s?t:Qe(t),this.dep.trigger())}}function he(e){return Te(e)?e.value:e}const yl={get:(e,t,n)=>t==="__v_raw"?e:he(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const i=e[t];return Te(i)&&!Te(n)?(i.value=n,!0):Reflect.set(e,t,n,s)}};function Dr(e){return $t(e)?e:new Proxy(e,yl)}class bl{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new ei(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=bn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ue!==this)return xr(this,!0),!0}get value(){const t=this.dep.track();return Cr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function _l(e,t,n=!1){let s,i;return W(e)?s=e:(s=e.get,i=e.set),new bl(s,i,n)}const In={},$n=new WeakMap;let Nt;function wl(e,t=!1,n=Nt){if(n){let s=$n.get(n);s||$n.set(n,s=[]),s.push(e)}}function xl(e,t,n=ae){const{immediate:s,deep:i,once:r,scheduler:o,augmentJob:a,call:l}=n,u=V=>i?V:Ke(V)||i===!1||i===0?yt(V,1):yt(V);let c,p,g,v,T=!1,C=!1;if(Te(e)?(p=()=>e.value,T=Ke(e)):$t(e)?(p=()=>u(e),T=!0):G(e)?(C=!0,T=e.some(V=>$t(V)||Ke(V)),p=()=>e.map(V=>{if(Te(V))return V.value;if($t(V))return u(V);if(W(V))return l?l(V,2):V()})):W(e)?t?p=l?()=>l(e,2):e:p=()=>{if(g){bt();try{g()}finally{_t()}}const V=Nt;Nt=c;try{return l?l(e,3,[v]):e(v)}finally{Nt=V}}:p=ft,t&&i){const V=p,Z=i===!0?1/0:i;p=()=>yt(V(),Z)}const y=Qo(),$=()=>{c.stop(),y&&y.active&&Ys(y.effects,c)};if(r&&t){const V=t;t=(...Z)=>{V(...Z),$()}}let E=C?new Array(e.length).fill(In):In;const N=V=>{if(!(!(c.flags&1)||!c.dirty&&!V))if(t){const Z=c.run();if(i||T||(C?Z.some((fe,re)=>ct(fe,E[re])):ct(Z,E))){g&&g();const fe=Nt;Nt=c;try{const re=[Z,E===In?void 0:C&&E[0]===In?[]:E,v];E=Z,l?l(t,3,re):t(...re)}finally{Nt=fe}}}else c.run()};return a&&a(N),c=new _r(p),c.scheduler=o?()=>o(N,!1):N,v=V=>wl(V,!1,c),g=c.onStop=()=>{const V=$n.get(c);if(V){if(l)l(V,4);else for(const Z of V)Z();$n.delete(c)}},t?s?N(!0):E=c.run():o?o(N.bind(null,!0),!0):c.run(),$.pause=c.pause.bind(c),$.resume=c.resume.bind(c),$.stop=$,$}function yt(e,t=1/0,n){if(t<=0||!ie(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,Te(e))yt(e.value,t,n);else if(G(e))for(let s=0;s{yt(s,t,n)});else if(mr(e)){for(const s in e)yt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&yt(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.35 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Tn(e,t,n,s){try{return s?e(...s):e()}catch(i){ss(i,t,n)}}function Xe(e,t,n,s){if(W(e)){const i=Tn(e,t,n,s);return i&&pr(i)&&i.catch(r=>{ss(r,t,n)}),i}if(G(e)){const i=[];for(let r=0;r>>1,i=Ie[s],r=wn(i);r=wn(n)?Ie.push(e):Ie.splice(Sl(t),0,e),e.flags|=1,Vr()}}function Vr(){Fn||(Fn=Hr.then(Fr))}function Cl(e){G(e)?qt.push(...e):Mt&&e.id===-1?Mt.splice(Gt+1,0,e):e.flags&1||(qt.push(e),e.flags|=1),Vr()}function bi(e,t,n=lt+1){for(;nwn(n)-wn(s));if(qt.length=0,Mt){Mt.push(...t);return}for(Mt=t,Gt=0;Gte.id==null?e.flags&2?-1:1/0:e.id;function Fr(e){try{for(lt=0;lt{s._d&&Gn(-1);const r=Bn(t);let o;try{o=e(...i)}finally{Bn(r),s._d&&Gn(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Al(e,t){if(Se===null)return e;const n=fs(Se),s=e.dirs||(e.dirs=[]);for(let i=0;i1)return n&&W(t)?t.call(s&&s.proxy):t}}const Rl=Symbol.for("v-scx"),Ml=()=>Ye(Rl);function Rd(e,t){return ri(e,null,t)}function Yt(e,t,n){return ri(e,t,n)}function ri(e,t,n=ae){const{immediate:s,deep:i,flush:r,once:o}=n,a=Ce({},n),l=t&&s||!t&&r!=="post";let u;if(Sn){if(r==="sync"){const v=Ml();u=v.__watcherHandles||(v.__watcherHandles=[])}else if(!l){const v=()=>{};return v.stop=ft,v.resume=ft,v.pause=ft,v}}const c=Me;a.call=(v,T,C)=>Xe(v,c,T,C);let p=!1;r==="post"?a.scheduler=v=>{De(v,c&&c.suspense)}:r!=="sync"&&(p=!0,a.scheduler=(v,T)=>{T?v():ii(v)}),a.augmentJob=v=>{t&&(v.flags|=4),p&&(v.flags|=2,c&&(v.id=c.uid,v.i=c))};const g=xl(e,t,a);return Sn&&(u?u.push(g):l&&g()),g}function Tl(e,t,n){const s=this.proxy,i=ce(e)?e.includes(".")?Ur(s,e):()=>s[e]:e.bind(s,s);let r;W(t)?r=t:(r=t.handler,n=t);const o=Pn(this),a=ri(i,r.bind(s),n);return o(),a}function Ur(e,t){const n=t.split(".");return()=>{let s=e;for(let i=0;ie.__isTeleport,bs=Symbol("_leaveCb");function oi(e,t){e.shapeFlag&6&&e.component?(e.transition=t,oi(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function we(e,t){return W(e)?Ce({name:e.name},t,{setup:e}):e}function jr(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function _i(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}const Un=new WeakMap;function mn(e,t,n,s,i=!1){if(G(e)){e.forEach((C,y)=>mn(C,t&&(G(t)?t[y]:t),n,s,i));return}if(Jt(s)&&!i){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&mn(e,t,n,s.component.subTree);return}const r=s.shapeFlag&4?fs(s.component):s.el,o=i?null:r,{i:a,r:l}=e,u=t&&t.r,c=a.refs===ae?a.refs={}:a.refs,p=a.setupState,g=ne(p),v=p===ae?dr:C=>_i(c,C)?!1:se(g,C),T=(C,y)=>!(y&&_i(c,y));if(u!=null&&u!==l){if(wi(t),ce(u))c[u]=null,v(u)&&(p[u]=null);else if(Te(u)){const C=t;T(u,C.k)&&(u.value=null),C.k&&(c[C.k]=null)}}if(W(l))Tn(l,a,12,[o,c]);else{const C=ce(l),y=Te(l);if(C||y){const $=()=>{if(e.f){const E=C?v(l)?p[l]:c[l]:T()||!e.k?l.value:c[e.k];if(i)G(E)&&Ys(E,r);else if(G(E))E.includes(r)||E.push(r);else if(C)c[l]=[r],v(l)&&(p[l]=c[l]);else{const N=[r];T(l,e.k)&&(l.value=N),e.k&&(c[e.k]=N)}}else C?(c[l]=o,v(l)&&(p[l]=o)):y&&(T(l,e.k)&&(l.value=o),e.k&&(c[e.k]=o))};if(o){const E=()=>{$(),Un.delete(e)};E.id=-1,Un.set(e,E),De(E,n)}else wi(e),$()}}}function wi(e){const t=Un.get(e);t&&(t.flags|=8,Un.delete(e))}ts().requestIdleCallback;ts().cancelIdleCallback;const Jt=e=>!!e.type.__asyncLoader,Gr=e=>e.type.__isKeepAlive;function Ol(e,t){Kr(e,"a",t)}function Il(e,t){Kr(e,"da",t)}function Kr(e,t,n=Me){const s=e.__wdc||(e.__wdc=()=>{let i=n;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(rs(t,s,n),n){let i=n.parent;for(;i&&i.parent;)Gr(i.parent.vnode)&&Nl(s,t,n,i),i=i.parent}}function Nl(e,t,n,s){const i=rs(t,e,s,!0);zr(()=>{Ys(s[t],i)},n)}function rs(e,t,n=Me,s=!1){if(n){const i=n[e]||(n[e]=[]),r=t.__weh||(t.__weh=(...o)=>{bt();const a=Pn(n),l=Xe(t,n,e,o);return a(),_t(),l});return s?i.unshift(r):i.push(r),r}}const Et=e=>(t,n=Me)=>{(!Sn||e==="sp")&&rs(e,(...s)=>t(...s),n)},Ll=Et("bm"),os=Et("m"),Dl=Et("bu"),Hl=Et("u"),ls=Et("bum"),zr=Et("um"),Vl=Et("sp"),$l=Et("rtg"),Fl=Et("rtc");function Bl(e,t=Me){rs("ec",e,t)}const Wr="components";function Ul(e,t){return Yr(Wr,e,!0,t)||e}const qr=Symbol.for("v-ndc");function jl(e){return ce(e)?Yr(Wr,e,!1)||e:e||qr}function Yr(e,t,n=!0,s=!1){const i=Se||Me;if(i){const r=i.type;{const a=Ma(r,!1);if(a&&(a===t||a===Ne(t)||a===Zn(Ne(t))))return r}const o=xi(i[e]||r[e],t)||xi(i.appContext[e],t);return!o&&s?r:o}}function xi(e,t){return e&&(e[t]||e[Ne(t)]||e[Zn(Ne(t))])}function Ft(e,t,n,s){let i;const r=n,o=G(e);if(o||ce(e)){const a=o&&$t(e);let l=!1,u=!1;a&&(l=!Ke(e),u=wt(e),e=ns(e)),i=new Array(e.length);for(let c=0,p=e.length;ct(a,l,void 0,r));else{const a=Object.keys(e);i=new Array(a.length);for(let l=0,u=a.length;l0;return t!=="default"&&(n.name=t),A(),We(j,null,[L("slot",n,s&&s())],u?-2:64)}let r=e[t];r&&r._c&&(r._d=!1),A();const o=r&&Jr(r(n)),a=n.key||o&&o.key,l=We(j,{key:(a&&!Je(a)?a:`_${t}`)+(!o&&s?"_fb":"")},o||(s?s():[]),o&&e._===1?64:-2);return l.scopeId&&(l.slotScopeIds=[l.scopeId+"-s"]),r&&r._c&&(r._d=!0),l}function Jr(e){return e.some(t=>En(t)?!(t.type===xt||t.type===j&&!Jr(t.children)):!0)?e:null}const Hs=e=>e?yo(e)?fs(e):Hs(e.parent):null,gn=Ce(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Hs(e.parent),$root:e=>Hs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Xr(e),$forceUpdate:e=>e.f||(e.f=()=>{ii(e.update)}),$nextTick:e=>e.n||(e.n=is.bind(e.proxy)),$watch:e=>Tl.bind(e)}),_s=(e,t)=>e!==ae&&!e.__isScriptSetup&&se(e,t),Gl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:i,props:r,accessCache:o,type:a,appContext:l}=e;if(t[0]!=="$"){const g=o[t];if(g!==void 0)switch(g){case 1:return s[t];case 2:return i[t];case 4:return n[t];case 3:return r[t]}else{if(_s(s,t))return o[t]=1,s[t];if(i!==ae&&se(i,t))return o[t]=2,i[t];if(se(r,t))return o[t]=3,r[t];if(n!==ae&&se(n,t))return o[t]=4,n[t];Vs&&(o[t]=0)}}const u=gn[t];let c,p;if(u)return t==="$attrs"&&Re(e.attrs,"get",""),u(e);if((c=a.__cssModules)&&(c=c[t]))return c;if(n!==ae&&se(n,t))return o[t]=4,n[t];if(p=l.config.globalProperties,se(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:i,ctx:r}=e;return _s(i,t)?(i[t]=n,!0):s!==ae&&se(s,t)?(s[t]=n,!0):se(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:i,props:r,type:o}},a){let l;return!!(n[a]||e!==ae&&a[0]!=="$"&&se(e,a)||_s(t,a)||se(r,a)||se(s,a)||se(gn,a)||se(i.config.globalProperties,a)||(l=o.__cssModules)&&l[a])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:se(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Kl(){return zl().slots}function zl(e){const t=vo();return t.setupContext||(t.setupContext=_o(t))}function Ei(e){return G(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Vs=!0;function Wl(e){const t=Xr(e),n=e.proxy,s=e.ctx;Vs=!1,t.beforeCreate&&Si(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:o,watch:a,provide:l,inject:u,created:c,beforeMount:p,mounted:g,beforeUpdate:v,updated:T,activated:C,deactivated:y,beforeDestroy:$,beforeUnmount:E,destroyed:N,unmounted:V,render:Z,renderTracked:fe,renderTriggered:re,errorCaptured:Ue,serverPrefetch:z,expose:D,inheritAttrs:q,components:et,directives:Le,filters:Pt}=t;if(u&&ql(u,s,null),o)for(const ee in o){const X=o[ee];W(X)&&(s[ee]=X.bind(n))}if(i){const ee=i.call(n,n);ie(ee)&&(e.data=Mn(ee))}if(Vs=!0,r)for(const ee in r){const X=r[ee],ze=W(X)?X.bind(n,n):W(X.get)?X.get.bind(n,n):ft,tt=!W(X)&&W(X.set)?X.set.bind(n):ft,je=_e({get:ze,set:tt});Object.defineProperty(s,ee,{enumerable:!0,configurable:!0,get:()=>je.value,set:Ae=>je.value=Ae})}if(a)for(const ee in a)Qr(a[ee],s,n,ee);if(l){const ee=W(l)?l.call(n):l;Reflect.ownKeys(ee).forEach(X=>{hn(X,ee[X])})}c&&Si(c,e,"c");function ge(ee,X){G(X)?X.forEach(ze=>ee(ze.bind(n))):X&&ee(X.bind(n))}if(ge(Ll,p),ge(os,g),ge(Dl,v),ge(Hl,T),ge(Ol,C),ge(Il,y),ge(Bl,Ue),ge(Fl,fe),ge($l,re),ge(ls,E),ge(zr,V),ge(Vl,z),G(D))if(D.length){const ee=e.exposed||(e.exposed={});D.forEach(X=>{Object.defineProperty(ee,X,{get:()=>n[X],set:ze=>n[X]=ze,enumerable:!0})})}else e.exposed||(e.exposed={});Z&&e.render===ft&&(e.render=Z),q!=null&&(e.inheritAttrs=q),et&&(e.components=et),Le&&(e.directives=Le),z&&jr(e)}function ql(e,t,n=ft){G(e)&&(e=$s(e));for(const s in e){const i=e[s];let r;ie(i)?"default"in i?r=Ye(i.from||s,i.default,!0):r=Ye(i.from||s):r=Ye(i),Te(r)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>r.value,set:o=>r.value=o}):t[s]=r}}function Si(e,t,n){Xe(G(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Qr(e,t,n,s){let i=s.includes(".")?Ur(n,s):()=>n[s];if(ce(e)){const r=t[e];W(r)&&Yt(i,r)}else if(W(e))Yt(i,e.bind(n));else if(ie(e))if(G(e))e.forEach(r=>Qr(r,t,n,s));else{const r=W(e.handler)?e.handler.bind(n):t[e.handler];W(r)&&Yt(i,r,e)}}function Xr(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:o}}=e.appContext,a=r.get(t);let l;return a?l=a:!i.length&&!n&&!s?l=t:(l={},i.length&&i.forEach(u=>jn(l,u,o,!0)),jn(l,t,o)),ie(t)&&r.set(t,l),l}function jn(e,t,n,s=!1){const{mixins:i,extends:r}=t;r&&jn(e,r,n,!0),i&&i.forEach(o=>jn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const a=Yl[o]||n&&n[o];e[o]=a?a(e[o],t[o]):t[o]}return e}const Yl={data:Ci,props:Ai,emits:Ai,methods:an,computed:an,beforeCreate:Pe,created:Pe,beforeMount:Pe,mounted:Pe,beforeUpdate:Pe,updated:Pe,beforeDestroy:Pe,beforeUnmount:Pe,destroyed:Pe,unmounted:Pe,activated:Pe,deactivated:Pe,errorCaptured:Pe,serverPrefetch:Pe,components:an,directives:an,watch:Ql,provide:Ci,inject:Jl};function Ci(e,t){return t?e?function(){return Ce(W(e)?e.call(this,this):e,W(t)?t.call(this,this):t)}:t:e}function Jl(e,t){return an($s(e),$s(t))}function $s(e){if(G(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ne(t)}Modifiers`]||e[`${Bt(t)}Modifiers`];function ta(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||ae;let i=n;const r=t.startsWith("update:"),o=r&&ea(s,t.slice(7));o&&(o.trim&&(i=n.map(c=>ce(c)?c.trim():c)),o.number&&(i=n.map(es)));let a,l=s[a=hs(t)]||s[a=hs(Ne(t))];!l&&r&&(l=s[a=hs(Bt(t))]),l&&Xe(l,e,6,i);const u=s[a+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[a])return;e.emitted[a]=!0,Xe(u,e,6,i)}}const na=new WeakMap;function eo(e,t,n=!1){const s=n?na:t.emitsCache,i=s.get(e);if(i!==void 0)return i;const r=e.emits;let o={},a=!1;if(!W(e)){const l=u=>{const c=eo(u,t,!0);c&&(a=!0,Ce(o,c))};!n&&t.mixins.length&&t.mixins.forEach(l),e.extends&&l(e.extends),e.mixins&&e.mixins.forEach(l)}return!r&&!a?(ie(e)&&s.set(e,null),null):(G(r)?r.forEach(l=>o[l]=null):Ce(o,r),ie(e)&&s.set(e,o),o)}function us(e,t){return!e||!Yn(t)?!1:(t=t.slice(2).replace(/Once$/,""),se(e,t[0].toLowerCase()+t.slice(1))||se(e,Bt(t))||se(e,t))}function Ri(e){const{type:t,vnode:n,proxy:s,withProxy:i,propsOptions:[r],slots:o,attrs:a,emit:l,render:u,renderCache:c,props:p,data:g,setupState:v,ctx:T,inheritAttrs:C}=e,y=Bn(e);let $,E;try{if(n.shapeFlag&4){const V=i||s,Z=V;$=ut(u.call(Z,V,c,p,v,g,T)),E=a}else{const V=t;$=ut(V.length>1?V(p,{attrs:a,slots:o,emit:l}):V(p,null)),E=t.props?a:sa(a)}}catch(V){vn.length=0,ss(V,e,1),$=L(xt)}let N=$;if(E&&C!==!1){const V=Object.keys(E),{shapeFlag:Z}=N;V.length&&Z&7&&(r&&V.some(Jn)&&(E=ia(E,r)),N=en(N,E,!1,!0))}return n.dirs&&(N=en(N,null,!1,!0),N.dirs=N.dirs?N.dirs.concat(n.dirs):n.dirs),n.transition&&oi(N,n.transition),$=N,Bn(y),$}const sa=e=>{let t;for(const n in e)(n==="class"||n==="style"||Yn(n))&&((t||(t={}))[n]=e[n]);return t},ia=(e,t)=>{const n={};for(const s in e)(!Jn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function ra(e,t,n){const{props:s,children:i,component:r}=e,{props:o,children:a,patchFlag:l}=t,u=r.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&l>=0){if(l&1024)return!0;if(l&16)return s?Mi(s,o,u):!!o;if(l&8){const c=t.dynamicProps;for(let p=0;pObject.create(no),io=e=>Object.getPrototypeOf(e)===no;function la(e,t,n,s=!1){const i={},r=so();e.propsDefaults=Object.create(null),ro(e,t,i,r);for(const o in e.propsOptions[0])o in i||(i[o]=void 0);n?e.props=s?i:Nr(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function aa(e,t,n,s){const{props:i,attrs:r,vnode:{patchFlag:o}}=e,a=ne(i),[l]=e.propsOptions;let u=!1;if((s||o>0)&&!(o&16)){if(o&8){const c=e.vnode.dynamicProps;for(let p=0;p{l=!0;const[g,v]=oo(p,t,!0);Ce(o,g),v&&a.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}if(!r&&!l)return ie(e)&&s.set(e,zt),zt;if(G(r))for(let c=0;ce==="_"||e==="_ctx"||e==="$stable",ai=e=>G(e)?e.map(ut):[ut(e)],ca=(e,t,n)=>{if(t._n)return t;const s=be((...i)=>ai(t(...i)),n);return s._c=!1,s},lo=(e,t,n)=>{const s=e._ctx;for(const i in e){if(li(i))continue;const r=e[i];if(W(r))t[i]=ca(i,r,s);else if(r!=null){const o=ai(r);t[i]=()=>o}}},ao=(e,t)=>{const n=ai(t);e.slots.default=()=>n},uo=(e,t,n)=>{for(const s in t)(n||!li(s))&&(e[s]=t[s])},fa=(e,t,n)=>{const s=e.slots=so();if(e.vnode.shapeFlag&32){const i=t._;i?(uo(s,t,n),n&&gr(s,"_",i,!0)):lo(t,s)}else t&&ao(e,t)},da=(e,t,n)=>{const{vnode:s,slots:i}=e;let r=!0,o=ae;if(s.shapeFlag&32){const a=t._;a?n&&a===1?r=!1:uo(i,t,n):(r=!t.$stable,lo(t,i)),o=t}else t&&(ao(e,t),o={default:1});if(r)for(const a in i)!li(a)&&o[a]==null&&delete i[a]},De=va;function pa(e){return ha(e)}function ha(e,t){const n=ts();n.__VUE__=!0;const{insert:s,remove:i,patchProp:r,createElement:o,createText:a,createComment:l,setText:u,setElementText:c,parentNode:p,nextSibling:g,setScopeId:v=ft,insertStaticContent:T}=e,C=(f,d,m,_=null,x=null,b=null,P=void 0,M=null,R=!!d.dynamicChildren)=>{if(f===d)return;f&&!on(f,d)&&(_=w(f),Ae(f,x,b,!0),f=null),d.patchFlag===-2&&(R=!1,d.dynamicChildren=null);const{type:S,ref:U,shapeFlag:I}=d;switch(S){case cs:y(f,d,m,_);break;case xt:$(f,d,m,_);break;case Hn:f==null&&E(d,m,_,P);break;case j:et(f,d,m,_,x,b,P,M,R);break;default:I&1?Z(f,d,m,_,x,b,P,M,R):I&6?Le(f,d,m,_,x,b,P,M,R):(I&64||I&128)&&S.process(f,d,m,_,x,b,P,M,R,F)}U!=null&&x?mn(U,f&&f.ref,b,d||f,!d):U==null&&f&&f.ref!=null&&mn(f.ref,null,b,f,!0)},y=(f,d,m,_)=>{if(f==null)s(d.el=a(d.children),m,_);else{const x=d.el=f.el;d.children!==f.children&&u(x,d.children)}},$=(f,d,m,_)=>{f==null?s(d.el=l(d.children||""),m,_):d.el=f.el},E=(f,d,m,_)=>{[f.el,f.anchor]=T(f.children,d,m,_,f.el,f.anchor)},N=({el:f,anchor:d},m,_)=>{let x;for(;f&&f!==d;)x=g(f),s(f,m,_),f=x;s(d,m,_)},V=({el:f,anchor:d})=>{let m;for(;f&&f!==d;)m=g(f),i(f),f=m;i(d)},Z=(f,d,m,_,x,b,P,M,R)=>{if(d.type==="svg"?P="svg":d.type==="math"&&(P="mathml"),f==null)fe(d,m,_,x,b,P,M,R);else{const S=f.el&&f.el._isVueCE?f.el:null;try{S&&S._beginPatch(),z(f,d,x,b,P,M,R)}finally{S&&S._endPatch()}}},fe=(f,d,m,_,x,b,P,M)=>{let R,S;const{props:U,shapeFlag:I,transition:B,dirs:K}=f;if(R=f.el=o(f.type,b,U&&U.is,U),I&8?c(R,f.children):I&16&&Ue(f.children,R,null,_,x,ws(f,b),P,M),K&&Ot(f,null,_,"created"),re(R,f,f.scopeId,P,_),U){for(const le in U)le!=="value"&&!fn(le)&&r(R,le,null,U[le],b,_);"value"in U&&r(R,"value",null,U.value,b),(S=U.onVnodeBeforeMount)&&rt(S,_,f)}K&&Ot(f,null,_,"beforeMount");const Q=ma(x,B);Q&&B.beforeEnter(R),s(R,d,m),((S=U&&U.onVnodeMounted)||Q||K)&&De(()=>{try{S&&rt(S,_,f),Q&&B.enter(R),K&&Ot(f,null,_,"mounted")}finally{}},x)},re=(f,d,m,_,x)=>{if(m&&v(f,m),_)for(let b=0;b<_.length;b++)v(f,_[b]);if(x){let b=x.subTree;if(d===b||ho(b.type)&&(b.ssContent===d||b.ssFallback===d)){const P=x.vnode;re(f,P,P.scopeId,P.slotScopeIds,x.parent)}}},Ue=(f,d,m,_,x,b,P,M,R=0)=>{for(let S=R;S{const M=d.el=f.el;let{patchFlag:R,dynamicChildren:S,dirs:U}=d;R|=f.patchFlag&16;const I=f.props||ae,B=d.props||ae;let K;if(m&&It(m,!1),(K=B.onVnodeBeforeUpdate)&&rt(K,m,d,f),U&&Ot(d,f,m,"beforeUpdate"),m&&It(m,!0),(I.innerHTML&&B.innerHTML==null||I.textContent&&B.textContent==null)&&c(M,""),S?D(f.dynamicChildren,S,M,m,_,ws(d,x),b):P||X(f,d,M,null,m,_,ws(d,x),b,!1),R>0){if(R&16)q(M,I,B,m,x);else if(R&2&&I.class!==B.class&&r(M,"class",null,B.class,x),R&4&&r(M,"style",I.style,B.style,x),R&8){const Q=d.dynamicProps;for(let le=0;le{K&&rt(K,m,d,f),U&&Ot(d,f,m,"updated")},_)},D=(f,d,m,_,x,b,P)=>{for(let M=0;M{if(d!==m){if(d!==ae)for(const b in d)!fn(b)&&!(b in m)&&r(f,b,d[b],null,x,_);for(const b in m){if(fn(b))continue;const P=m[b],M=d[b];P!==M&&b!=="value"&&r(f,b,M,P,x,_)}"value"in m&&r(f,"value",d.value,m.value,x)}},et=(f,d,m,_,x,b,P,M,R)=>{const S=d.el=f?f.el:a(""),U=d.anchor=f?f.anchor:a("");let{patchFlag:I,dynamicChildren:B,slotScopeIds:K}=d;K&&(M=M?M.concat(K):K),f==null?(s(S,m,_),s(U,m,_),Ue(d.children||[],m,U,x,b,P,M,R)):I>0&&I&64&&B&&f.dynamicChildren&&f.dynamicChildren.length===B.length?(D(f.dynamicChildren,B,m,x,b,P,M),(d.key!=null||x&&d===x.subTree)&&co(f,d,!0)):X(f,d,m,U,x,b,P,M,R)},Le=(f,d,m,_,x,b,P,M,R)=>{d.slotScopeIds=M,f==null?d.shapeFlag&512?x.ctx.activate(d,m,_,P,R):Pt(d,m,_,x,b,P,R):St(f,d,R)},Pt=(f,d,m,_,x,b,P)=>{const M=f.component=Sa(f,_,x);if(Gr(f)&&(M.ctx.renderer=F),Ca(M,!1,P),M.asyncDep){if(x&&x.registerDep(M,ge,P),!f.el){const R=M.subTree=L(xt);$(null,R,d,m),f.placeholder=R.el}}else ge(M,f,d,m,x,b,P)},St=(f,d,m)=>{const _=d.component=f.component;if(ra(f,d,m))if(_.asyncDep&&!_.asyncResolved){ee(_,d,m);return}else _.next=d,_.update();else d.el=f.el,_.vnode=d},ge=(f,d,m,_,x,b,P)=>{const M=()=>{if(f.isMounted){let{next:I,bu:B,u:K,parent:Q,vnode:le}=f;{const st=fo(f);if(st){I&&(I.el=le.el,ee(f,I,P)),st.asyncDep.then(()=>{De(()=>{f.isUnmounted||S()},x)});return}}let oe=I,ve;It(f,!1),I?(I.el=le.el,ee(f,I,P)):I=le,B&&Dn(B),(ve=I.props&&I.props.onVnodeBeforeUpdate)&&rt(ve,Q,I,le),It(f,!0);const xe=Ri(f),nt=f.subTree;f.subTree=xe,C(nt,xe,p(nt.el),w(nt),f,x,b),I.el=xe.el,oe===null&&oa(f,xe.el),K&&De(K,x),(ve=I.props&&I.props.onVnodeUpdated)&&De(()=>rt(ve,Q,I,le),x)}else{let I;const{el:B,props:K}=d,{bm:Q,m:le,parent:oe,root:ve,type:xe}=f,nt=Jt(d);It(f,!1),Q&&Dn(Q),!nt&&(I=K&&K.onVnodeBeforeMount)&&rt(I,oe,d),It(f,!0);{ve.ce&&ve.ce._hasShadowRoot()&&ve.ce._injectChildStyle(xe,f.parent?f.parent.type:void 0);const st=f.subTree=Ri(f);C(null,st,m,_,f,x,b),d.el=st.el}if(le&&De(le,x),!nt&&(I=K&&K.onVnodeMounted)){const st=d;De(()=>rt(I,oe,st),x)}(d.shapeFlag&256||oe&&Jt(oe.vnode)&&oe.vnode.shapeFlag&256)&&f.a&&De(f.a,x),f.isMounted=!0,d=m=_=null}};f.scope.on();const R=f.effect=new _r(M);f.scope.off();const S=f.update=R.run.bind(R),U=f.job=R.runIfDirty.bind(R);U.i=f,U.id=f.uid,R.scheduler=()=>ii(U),It(f,!0),S()},ee=(f,d,m)=>{d.component=f;const _=f.vnode.props;f.vnode=d,f.next=null,aa(f,d.props,_,m),da(f,d.children,m),bt(),bi(f),_t()},X=(f,d,m,_,x,b,P,M,R=!1)=>{const S=f&&f.children,U=f?f.shapeFlag:0,I=d.children,{patchFlag:B,shapeFlag:K}=d;if(B>0){if(B&128){tt(S,I,m,_,x,b,P,M,R);return}else if(B&256){ze(S,I,m,_,x,b,P,M,R);return}}K&8?(U&16&&Ge(S,x,b),I!==S&&c(m,I)):U&16?K&16?tt(S,I,m,_,x,b,P,M,R):Ge(S,x,b,!0):(U&8&&c(m,""),K&16&&Ue(I,m,_,x,b,P,M,R))},ze=(f,d,m,_,x,b,P,M,R)=>{f=f||zt,d=d||zt;const S=f.length,U=d.length,I=Math.min(S,U);let B;for(B=0;BU?Ge(f,x,b,!0,!1,I):Ue(d,m,_,x,b,P,M,R,I)},tt=(f,d,m,_,x,b,P,M,R)=>{let S=0;const U=d.length;let I=f.length-1,B=U-1;for(;S<=I&&S<=B;){const K=f[S],Q=d[S]=R?gt(d[S]):ut(d[S]);if(on(K,Q))C(K,Q,m,null,x,b,P,M,R);else break;S++}for(;S<=I&&S<=B;){const K=f[I],Q=d[B]=R?gt(d[B]):ut(d[B]);if(on(K,Q))C(K,Q,m,null,x,b,P,M,R);else break;I--,B--}if(S>I){if(S<=B){const K=B+1,Q=KB)for(;S<=I;)Ae(f[S],x,b,!0),S++;else{const K=S,Q=S,le=new Map;for(S=Q;S<=B;S++){const Ve=d[S]=R?gt(d[S]):ut(d[S]);Ve.key!=null&&le.set(Ve.key,S)}let oe,ve=0;const xe=B-Q+1;let nt=!1,st=0;const sn=new Array(xe);for(S=0;S=xe){Ae(Ve,x,b,!0);continue}let it;if(Ve.key!=null)it=le.get(Ve.key);else for(oe=Q;oe<=B;oe++)if(sn[oe-Q]===0&&on(Ve,d[oe])){it=oe;break}it===void 0?Ae(Ve,x,b,!0):(sn[it-Q]=S+1,it>=st?st=it:nt=!0,C(Ve,d[it],m,null,x,b,P,M,R),ve++)}const di=nt?ga(sn):zt;for(oe=di.length-1,S=xe-1;S>=0;S--){const Ve=Q+S,it=d[Ve],pi=d[Ve+1],hi=Ve+1{const{el:b,type:P,transition:M,children:R,shapeFlag:S}=f;if(S&6){je(f.component.subTree,d,m,_);return}if(S&128){f.suspense.move(d,m,_);return}if(S&64){P.move(f,d,m,F);return}if(P===j){s(b,d,m);for(let I=0;IM.enter(b),x));else{const{leave:I,delayLeave:B,afterLeave:K}=M,Q=()=>{f.ctx.isUnmounted?i(b):s(b,d,m)},le=()=>{const oe=b._isLeaving||!!b[bs];b._isLeaving&&b[bs](!0),M.persisted&&!oe?Q():I(b,()=>{Q(),K&&K()})};B?B(b,Q,le):le()}else s(b,d,m)},Ae=(f,d,m,_=!1,x=!1)=>{const{type:b,props:P,ref:M,children:R,dynamicChildren:S,shapeFlag:U,patchFlag:I,dirs:B,cacheIndex:K,memo:Q}=f;if(I===-2&&(x=!1),M!=null&&(bt(),mn(M,null,m,f,!0),_t()),K!=null&&(d.renderCache[K]=void 0),U&256){d.ctx.deactivate(f);return}const le=U&1&&B,oe=!Jt(f);let ve;if(oe&&(ve=P&&P.onVnodeBeforeUnmount)&&rt(ve,d,f),U&6)kt(f.component,m,_);else{if(U&128){f.suspense.unmount(m,_);return}le&&Ot(f,null,d,"beforeUnmount"),U&64?f.type.remove(f,d,m,F,_):S&&!S.hasOnce&&(b!==j||I>0&&I&64)?Ge(S,d,m,!1,!0):(b===j&&I&384||!x&&U&16)&&Ge(R,d,m),_&&Ct(f)}const xe=Q!=null&&K==null;(oe&&(ve=P&&P.onVnodeUnmounted)||le||xe)&&De(()=>{ve&&rt(ve,d,f),le&&Ot(f,null,d,"unmounted"),xe&&(f.el=null)},m)},Ct=f=>{const{type:d,el:m,anchor:_,transition:x}=f;if(d===j){At(m,_);return}if(d===Hn){V(f);return}const b=()=>{i(m),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(f.shapeFlag&1&&x&&!x.persisted){const{leave:P,delayLeave:M}=x,R=()=>P(m,b);M?M(f.el,b,R):R()}else b()},At=(f,d)=>{let m;for(;f!==d;)m=g(f),i(f),f=m;i(d)},kt=(f,d,m)=>{const{bum:_,scope:x,job:b,subTree:P,um:M,m:R,a:S}=f;Pi(R),Pi(S),_&&Dn(_),x.stop(),b&&(b.flags|=8,Ae(P,f,d,m)),M&&De(M,d),De(()=>{f.isUnmounted=!0},d)},Ge=(f,d,m,_=!1,x=!1,b=0)=>{for(let P=b;P{if(f.shapeFlag&6)return w(f.component.subTree);if(f.shapeFlag&128)return f.suspense.next();const d=g(f.anchor||f.el),m=d&&d[Pl];return m?g(m):d};let H=!1;const O=(f,d,m)=>{let _;f==null?d._vnode&&(Ae(d._vnode,null,null,!0),_=d._vnode.component):C(d._vnode||null,f,d,null,null,null,m),d._vnode=f,H||(H=!0,bi(_),$r(),H=!1)},F={p:C,um:Ae,m:je,r:Ct,mt:Pt,mc:Ue,pc:X,pbc:D,n:w,o:e};return{render:O,hydrate:void 0,createApp:Zl(O)}}function ws({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function It({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function ma(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function co(e,t,n=!1){const s=e.children,i=t.children;if(G(s)&&G(i))for(let r=0;r>1,e[n[a]]0&&(t[s]=n[r-1]),n[r]=s)}}for(r=n.length,o=n[r-1];r-- >0;)n[r]=o,o=t[o];return n}function fo(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:fo(t)}function Pi(e){if(e)for(let t=0;te.__isSuspense;function va(e,t){t&&t.pendingBranch?G(e)?t.effects.push(...e):t.effects.push(e):Cl(e)}const j=Symbol.for("v-fgt"),cs=Symbol.for("v-txt"),xt=Symbol.for("v-cmt"),Hn=Symbol.for("v-stc"),vn=[];let Fe=null;function A(e=!1){vn.push(Fe=e?null:[])}function ya(){vn.pop(),Fe=vn[vn.length-1]||null}let xn=1;function Gn(e,t=!1){xn+=e,e<0&&Fe&&t&&(Fe.hasOnce=!0)}function mo(e){return e.dynamicChildren=xn>0?Fe||zt:null,ya(),xn>0&&Fe&&Fe.push(e),e}function k(e,t,n,s,i,r){return mo(h(e,t,n,s,i,r,!0))}function We(e,t,n,s,i){return mo(L(e,t,n,s,i,!0))}function En(e){return e?e.__v_isVNode===!0:!1}function on(e,t){return e.type===t.type&&e.key===t.key}const go=({key:e})=>e??null,Vn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ce(e)||Te(e)||W(e)?{i:Se,r:e,k:t,f:!!n}:e:null);function h(e,t=null,n=null,s=0,i=null,r=e===j?0:1,o=!1,a=!1){const l={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&go(t),ref:t&&Vn(t),scopeId:Br,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:s,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:Se};return a?(ui(l,n),r&128&&e.normalize(l)):n&&(l.shapeFlag|=ce(n)?8:16),xn>0&&!o&&Fe&&(l.patchFlag>0||r&6)&&l.patchFlag!==32&&Fe.push(l),l}const L=ba;function ba(e,t=null,n=null,s=0,i=null,r=!1){if((!e||e===qr)&&(e=xt),En(e)){const a=en(e,t,!0);return n&&ui(a,n),xn>0&&!r&&Fe&&(a.shapeFlag&6?Fe[Fe.indexOf(e)]=a:Fe.push(a)),a.patchFlag=-2,a}if(Ta(e)&&(e=e.__vccOpts),t){t=_a(t);let{class:a,style:l}=t;a&&!ce(a)&&(t.class=Be(a)),ie(l)&&(si(l)&&!G(l)&&(l=Ce({},l)),t.style=dt(l))}const o=ce(e)?1:ho(e)?128:kl(e)?64:ie(e)?4:W(e)?2:0;return h(e,t,n,s,i,o,r,!0)}function _a(e){return e?si(e)||io(e)?Ce({},e):e:null}function en(e,t,n=!1,s=!1){const{props:i,ref:r,patchFlag:o,children:a,transition:l}=e,u=t?wa(i||{},t):i,c={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&go(u),ref:t&&t.ref?n&&r?G(r)?r.concat(Vn(t)):[r,Vn(t)]:Vn(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:a,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==j?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:l,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&en(e.ssContent),ssFallback:e.ssFallback&&en(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return l&&s&&oi(c,l.clone(c)),c}function pe(e=" ",t=0){return L(cs,null,e,t)}function Lt(e,t){const n=L(Hn,null,e);return n.staticCount=t,n}function He(e="",t=!1){return t?(A(),We(xt,null,e)):L(xt,null,e)}function ut(e){return e==null||typeof e=="boolean"?L(xt):G(e)?L(j,null,e.slice()):En(e)?gt(e):L(cs,null,String(e))}function gt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:en(e)}function ui(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(G(t))n=16;else if(typeof t=="object")if(s&65){const i=t.default;i&&(i._c&&(i._d=!1),ui(e,i()),i._c&&(i._d=!0));return}else{n=32;const i=t._;!i&&!io(t)?t._ctx=Se:i===3&&Se&&(Se.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else W(t)?(t={default:t,_ctx:Se},n=32):(t=String(t),s&64?(n=16,t=[pe(t)]):n=8);e.children=t,e.shapeFlag|=n}function wa(...e){const t={};for(let n=0;nMe||Se;let Kn,Bs;{const e=ts(),t=(n,s)=>{let i;return(i=e[n])||(i=e[n]=[]),i.push(s),r=>{i.length>1?i.forEach(o=>o(r)):i[0](r)}};Kn=t("__VUE_INSTANCE_SETTERS__",n=>Me=n),Bs=t("__VUE_SSR_SETTERS__",n=>Sn=n)}const Pn=e=>{const t=Me;return Kn(e),e.scope.on(),()=>{e.scope.off(),Kn(t)}},ki=()=>{Me&&Me.scope.off(),Kn(null)};function yo(e){return e.vnode.shapeFlag&4}let Sn=!1;function Ca(e,t=!1,n=!1){t&&Bs(t);const{props:s,children:i}=e.vnode,r=yo(e);la(e,s,r,t),fa(e,i,n||t);const o=r?Aa(e,t):void 0;return t&&Bs(!1),o}function Aa(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Gl);const{setup:s}=n;if(s){bt();const i=e.setupContext=s.length>1?_o(e):null,r=Pn(e),o=Tn(s,e,0,[e.props,i]),a=pr(o);if(_t(),r(),(a||e.sp)&&!Jt(e)&&jr(e),a){if(o.then(ki,ki),t)return o.then(l=>{Oi(e,l)}).catch(l=>{ss(l,e,0)});e.asyncDep=o}else Oi(e,o)}else bo(e)}function Oi(e,t,n){W(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ie(t)&&(e.setupState=Dr(t)),bo(e)}function bo(e,t,n){const s=e.type;e.render||(e.render=s.render||ft);{const i=Pn(e);bt();try{Wl(e)}finally{_t(),i()}}}const Ra={get(e,t){return Re(e,"get",""),e[t]}};function _o(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Ra),slots:e.slots,emit:e.emit,expose:t}}function fs(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Dr(ml(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in gn)return gn[n](e)},has(t,n){return n in t||n in gn}})):e.proxy}function Ma(e,t=!0){return W(e)?e.displayName||e.name:e.name||t&&e.__name}function Ta(e){return W(e)&&"__vccOpts"in e}const _e=(e,t)=>_l(e,t,Sn);function wo(e,t,n){try{Gn(-1);const s=arguments.length;return s===2?ie(t)&&!G(t)?En(t)?L(e,null,[t]):L(e,t):L(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&En(n)&&(n=[n]),L(e,t,n))}finally{Gn(1)}}const Pa="3.5.35";/** +* @vue/runtime-dom v3.5.35 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Us;const Ii=typeof window<"u"&&window.trustedTypes;if(Ii)try{Us=Ii.createPolicy("vue",{createHTML:e=>e})}catch{}const xo=Us?e=>Us.createHTML(e):e=>e,ka="http://www.w3.org/2000/svg",Oa="http://www.w3.org/1998/Math/MathML",mt=typeof document<"u"?document:null,Ni=mt&&mt.createElement("template"),Ia={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const i=t==="svg"?mt.createElementNS(ka,e):t==="mathml"?mt.createElementNS(Oa,e):n?mt.createElement(e,{is:n}):mt.createElement(e);return e==="select"&&s&&s.multiple!=null&&i.setAttribute("multiple",s.multiple),i},createText:e=>mt.createTextNode(e),createComment:e=>mt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>mt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,i,r){const o=n?n.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),!(i===r||!(i=i.nextSibling)););else{Ni.innerHTML=xo(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const a=Ni.content;if(s==="svg"||s==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Na=Symbol("_vtc");function La(e,t,n){const s=e[Na];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Li=Symbol("_vod"),Da=Symbol("_vsh"),Ha=Symbol(""),Va=/(?:^|;)\s*display\s*:/;function $a(e,t,n){const s=e.style,i=ce(n);let r=!1;if(n&&!i){if(t)if(ce(t))for(const o of t.split(";")){const a=o.slice(0,o.indexOf(":")).trim();n[a]==null&&un(s,a,"")}else for(const o in t)n[o]==null&&un(s,o,"");for(const o in n){o==="display"&&(r=!0);const a=n[o];a!=null?Ba(e,o,!ce(t)&&t?t[o]:void 0,a)||un(s,o,a):un(s,o,"")}}else if(i){if(t!==n){const o=s[Ha];o&&(n+=";"+o),s.cssText=n,r=Va.test(n)}}else t&&e.removeAttribute("style");Li in e&&(e[Li]=r?s.display:"",e[Da]&&(s.display="none"))}const Di=/\s*!important$/;function un(e,t,n){if(G(n))n.forEach(s=>un(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Fa(e,t);Di.test(n)?e.setProperty(Bt(s),n.replace(Di,""),"important"):e[s]=n}}const Hi=["Webkit","Moz","ms"],xs={};function Fa(e,t){const n=xs[t];if(n)return n;let s=Ne(t);if(s!=="filter"&&s in e)return xs[t]=s;s=Zn(s);for(let i=0;iEs||(Ka.then(()=>Es=0),Es=Date.now());function Wa(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;const i=n.value;if(G(i)){const r=s.stopImmediatePropagation;s.stopImmediatePropagation=()=>{r.call(s),s._stopped=!0};const o=i.slice(),a=[s];for(let l=0;le.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,qa=(e,t,n,s,i,r)=>{const o=i==="svg";t==="class"?La(e,s,o):t==="style"?$a(e,n,s):Yn(t)?Jn(t)||ja(e,t,n,s,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ya(e,t,s,o))?(Fi(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&$i(e,t,s,o,r,t!=="value")):e._isVueCE&&(Ja(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ce(s)))?Fi(e,Ne(t),s,r,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),$i(e,t,s,o))};function Ya(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&ji(t)&&W(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return ji(t)&&ce(n)?!1:t in e}function Ja(e,t){const n=e._def.props;if(!n)return!1;const s=Ne(t);return Array.isArray(n)?n.some(i=>Ne(i)===s):Object.keys(n).some(i=>Ne(i)===s)}const zn=e=>{const t=e.props["onUpdate:modelValue"]||!1;return G(t)?n=>Dn(t,n):t};function Qa(e){e.target.composing=!0}function Gi(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Xt=Symbol("_assign");function Ki(e,t,n){return t&&(e=e.trim()),n&&(e=es(e)),e}const Md={created(e,{modifiers:{lazy:t,trim:n,number:s}},i){e[Xt]=zn(i);const r=s||i.props&&i.props.type==="number";Dt(e,t?"change":"input",o=>{o.target.composing||e[Xt](Ki(e.value,n,r))}),(n||r)&&Dt(e,"change",()=>{e.value=Ki(e.value,n,r)}),t||(Dt(e,"compositionstart",Qa),Dt(e,"compositionend",Gi),Dt(e,"change",Gi))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:i,number:r}},o){if(e[Xt]=zn(o),e.composing)return;const a=(r||e.type==="number")&&!/^0\d/.test(e.value)?es(e.value):e.value,l=t??"";if(a===l)return;const u=e.getRootNode();(u instanceof Document||u instanceof ShadowRoot)&&u.activeElement===e&&e.type!=="range"&&(s&&t===n||i&&e.value.trim()===l)||(e.value=l)}},Xa={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const i=Qn(t);Dt(e,"change",()=>{const r=Array.prototype.filter.call(e.options,o=>o.selected).map(o=>n?es(Wn(o)):Wn(o));e[Xt](e.multiple?i?new Set(r):r:r[0]),e._assigning=!0,is(()=>{e._assigning=!1})}),e[Xt]=zn(s)},mounted(e,{value:t}){zi(e,t)},beforeUpdate(e,t,n){e[Xt]=zn(n)},updated(e,{value:t}){e._assigning||zi(e,t)}};function zi(e,t){const n=e.multiple,s=G(t);if(!(n&&!s&&!Qn(t))){for(let i=0,r=e.options.length;iString(u)===String(a)):o.selected=Yo(t,a)>-1}else o.selected=t.has(a);else if(Rn(Wn(o),t)){e.selectedIndex!==i&&(e.selectedIndex=i);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Wn(e){return"_value"in e?e._value:e.value}const Za=["ctrl","shift","alt","meta"],eu={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Za.some(n=>e[`${n}Key`]&&!t.includes(n))},Eo=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=((i,...r)=>{for(let o=0;o{const t=nu().createApp(...e),{mount:n}=t;return t.mount=s=>{const i=ru(s);if(!i)return;const r=t._component;!W(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const o=n(i,!1,iu(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),o},t});function iu(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function ru(e){return ce(e)?document.querySelector(e):e}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Kt=typeof document<"u";function So(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function ou(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&So(e.default)}const te=Object.assign;function Ss(e,t){const n={};for(const s in t){const i=t[s];n[s]=Ze(i)?i.map(e):e(i)}return n}const yn=()=>{},Ze=Array.isArray;function qi(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const Co=/#/g,lu=/&/g,au=/\//g,uu=/=/g,cu=/\?/g,Ao=/\+/g,fu=/%5B/g,du=/%5D/g,Ro=/%5E/g,pu=/%60/g,Mo=/%7B/g,hu=/%7C/g,To=/%7D/g,mu=/%20/g;function ci(e){return e==null?"":encodeURI(""+e).replace(hu,"|").replace(fu,"[").replace(du,"]")}function gu(e){return ci(e).replace(Mo,"{").replace(To,"}").replace(Ro,"^")}function js(e){return ci(e).replace(Ao,"%2B").replace(mu,"+").replace(Co,"%23").replace(lu,"%26").replace(pu,"`").replace(Mo,"{").replace(To,"}").replace(Ro,"^")}function vu(e){return js(e).replace(uu,"%3D")}function yu(e){return ci(e).replace(Co,"%23").replace(cu,"%3F")}function bu(e){return yu(e).replace(au,"%2F")}function Cn(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const _u=/\/$/,wu=e=>e.replace(_u,"");function Cs(e,t,n="/"){let s,i={},r="",o="";const a=t.indexOf("#");let l=t.indexOf("?");return l=a>=0&&l>a?-1:l,l>=0&&(s=t.slice(0,l),r=t.slice(l,a>0?a:t.length),i=e(r.slice(1))),a>=0&&(s=s||t.slice(0,a),o=t.slice(a,t.length)),s=Cu(s??t,n),{fullPath:s+r+o,path:s,query:i,hash:Cn(o)}}function xu(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Yi(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Eu(e,t,n){const s=t.matched.length-1,i=n.matched.length-1;return s>-1&&s===i&&tn(t.matched[s],n.matched[i])&&Po(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function tn(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Po(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Su(e[n],t[n]))return!1;return!0}function Su(e,t){return Ze(e)?Ji(e,t):Ze(t)?Ji(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function Ji(e,t){return Ze(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function Cu(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),i=s[s.length-1];(i===".."||i===".")&&s.push("");let r=n.length-1,o,a;for(o=0;o1&&r--;else break;return n.slice(0,r).join("/")+"/"+s.slice(o).join("/")}const Rt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Gs=(function(e){return e.pop="pop",e.push="push",e})({}),As=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function Au(e){if(!e)if(Kt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),wu(e)}const Ru=/^[^#]+#/;function Mu(e,t){return e.replace(Ru,"#")+t}function Tu(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const ds=()=>({left:window.scrollX,top:window.scrollY});function Pu(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),i=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=Tu(i,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Qi(e,t){return(history.state?history.state.position-t:-1)+e}const Ks=new Map;function ku(e,t){Ks.set(e,t)}function Ou(e){const t=Ks.get(e);return Ks.delete(e),t}function Iu(e){return typeof e=="string"||e&&typeof e=="object"}function ko(e){return typeof e=="string"||typeof e=="symbol"}let de=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const Oo=Symbol("");de.MATCHER_NOT_FOUND+"",de.NAVIGATION_GUARD_REDIRECT+"",de.NAVIGATION_ABORTED+"",de.NAVIGATION_CANCELLED+"",de.NAVIGATION_DUPLICATED+"";function nn(e,t){return te(new Error,{type:e,[Oo]:!0},t)}function ht(e,t){return e instanceof Error&&Oo in e&&(t==null||!!(e.type&t))}const Nu=["params","query","hash"];function Lu(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of Nu)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function Du(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;si&&js(i)):[s&&js(s)]).forEach(i=>{i!==void 0&&(t+=(t.length?"&":"")+n,i!=null&&(t+="="+i))})}return t}function Hu(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=Ze(s)?s.map(i=>i==null?null:""+i):s==null?s:""+s)}return t}const Vu=Symbol(""),Zi=Symbol(""),ps=Symbol(""),fi=Symbol(""),zs=Symbol("");function ln(){let e=[];function t(s){return e.push(s),()=>{const i=e.indexOf(s);i>-1&&e.splice(i,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Tt(e,t,n,s,i,r=o=>o()){const o=s&&(s.enterCallbacks[i]=s.enterCallbacks[i]||[]);return()=>new Promise((a,l)=>{const u=g=>{g===!1?l(nn(de.NAVIGATION_ABORTED,{from:n,to:t})):g instanceof Error?l(g):Iu(g)?l(nn(de.NAVIGATION_GUARD_REDIRECT,{from:t,to:g})):(o&&s.enterCallbacks[i]===o&&typeof g=="function"&&o.push(g),a())},c=r(()=>e.call(s&&s.instances[i],t,n,u));let p=Promise.resolve(c);e.length<3&&(p=p.then(u)),p.catch(g=>l(g))})}function Rs(e,t,n,s,i=r=>r()){const r=[];for(const o of e)for(const a in o.components){let l=o.components[a];if(!(t!=="beforeRouteEnter"&&!o.instances[a]))if(So(l)){const u=(l.__vccOpts||l)[t];u&&r.push(Tt(u,n,s,o,a,i))}else{let u=l();r.push(()=>u.then(c=>{if(!c)throw new Error(`Couldn't resolve component "${a}" at "${o.path}"`);const p=ou(c)?c.default:c;o.mods[a]=c,o.components[a]=p;const g=(p.__vccOpts||p)[t];return g&&Tt(g,n,s,o,a,i)()}))}}return r}function $u(e,t){const n=[],s=[],i=[],r=Math.max(t.matched.length,e.matched.length);for(let o=0;otn(u,a))?s.push(a):n.push(a));const l=e.matched[o];l&&(t.matched.find(u=>tn(u,l))||i.push(l))}return[n,s,i]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let Fu=()=>location.protocol+"//"+location.host;function Io(e,t){const{pathname:n,search:s,hash:i}=t,r=e.indexOf("#");if(r>-1){let o=i.includes(e.slice(r))?e.slice(r).length:1,a=i.slice(o);return a[0]!=="/"&&(a="/"+a),Yi(a,"")}return Yi(n,e)+s+i}function Bu(e,t,n,s){let i=[],r=[],o=null;const a=({state:g})=>{const v=Io(e,location),T=n.value,C=t.value;let y=0;if(g){if(n.value=v,t.value=g,o&&o===T){o=null;return}y=C?g.position-C.position:0}else s(v);i.forEach($=>{$(n.value,T,{delta:y,type:Gs.pop,direction:y?y>0?As.forward:As.back:As.unknown})})};function l(){o=n.value}function u(g){i.push(g);const v=()=>{const T=i.indexOf(g);T>-1&&i.splice(T,1)};return r.push(v),v}function c(){if(document.visibilityState==="hidden"){const{history:g}=window;if(!g.state)return;g.replaceState(te({},g.state,{scroll:ds()}),"")}}function p(){for(const g of r)g();r=[],window.removeEventListener("popstate",a),window.removeEventListener("pagehide",c),document.removeEventListener("visibilitychange",c)}return window.addEventListener("popstate",a),window.addEventListener("pagehide",c),document.addEventListener("visibilitychange",c),{pauseListeners:l,listen:u,destroy:p}}function er(e,t,n,s=!1,i=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:i?ds():null}}function Uu(e){const{history:t,location:n}=window,s={value:Io(e,n)},i={value:t.state};i.value||r(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function r(l,u,c){const p=e.indexOf("#"),g=p>-1?(n.host&&document.querySelector("base")?e:e.slice(p))+l:Fu()+e+l;try{t[c?"replaceState":"pushState"](u,"",g),i.value=u}catch(v){console.error(v),n[c?"replace":"assign"](g)}}function o(l,u){r(l,te({},t.state,er(i.value.back,l,i.value.forward,!0),u,{position:i.value.position}),!0),s.value=l}function a(l,u){const c=te({},i.value,t.state,{forward:l,scroll:ds()});r(c.current,c,!0),r(l,te({},er(s.value,l,null),{position:c.position+1},u),!1),s.value=l}return{location:s,state:i,push:a,replace:o}}function ju(e){e=Au(e);const t=Uu(e),n=Bu(e,t.state,t.location,t.replace);function s(r,o=!0){o||n.pauseListeners(),history.go(r)}const i=te({location:"",base:e,go:s,createHref:Mu.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function Gu(e){return e=location.host?e||location.pathname+location.search:"",e.includes("#")||(e+="#"),ju(e)}let Ht=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var ye=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(ye||{});const Ku={type:Ht.Static,value:""},zu=/[a-zA-Z0-9_]/;function Wu(e){if(!e)return[[]];if(e==="/")return[[Ku]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(v){throw new Error(`ERR (${n})/"${u}": ${v}`)}let n=ye.Static,s=n;const i=[];let r;function o(){r&&i.push(r),r=[]}let a=0,l,u="",c="";function p(){u&&(n===ye.Static?r.push({type:Ht.Static,value:u}):n===ye.Param||n===ye.ParamRegExp||n===ye.ParamRegExpEnd?(r.length>1&&(l==="*"||l==="+")&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),r.push({type:Ht.Param,value:u,regexp:c,repeatable:l==="*"||l==="+",optional:l==="*"||l==="?"})):t("Invalid state to consume buffer"),u="")}function g(){u+=l}for(;at.length?t.length===1&&t[0]===ke.Static+ke.Segment?1:-1:0}function No(e,t){let n=0;const s=e.score,i=t.score;for(;n0&&t[t.length-1]<0}const Xu={strict:!1,end:!0,sensitive:!1};function Zu(e,t,n){const s=Ju(Wu(e.path),n),i=te(s,{record:e,parent:t,children:[],alias:[]});return t&&!i.record.aliasOf==!t.record.aliasOf&&t.children.push(i),i}function ec(e,t){const n=[],s=new Map;t=qi(Xu,t);function i(p){return s.get(p)}function r(p,g,v){const T=!v,C=ir(p);C.aliasOf=v&&v.record;const y=qi(t,p),$=[C];if("alias"in p){const V=typeof p.alias=="string"?[p.alias]:p.alias;for(const Z of V)$.push(ir(te({},C,{components:v?v.record.components:C.components,path:Z,aliasOf:v?v.record:C})))}let E,N;for(const V of $){const{path:Z}=V;if(g&&Z[0]!=="/"){const fe=g.record.path,re=fe[fe.length-1]==="/"?"":"/";V.path=g.record.path+(Z&&re+Z)}if(E=Zu(V,g,y),v?v.alias.push(E):(N=N||E,N!==E&&N.alias.push(E),T&&p.name&&!rr(E)&&o(p.name)),Lo(E)&&l(E),C.children){const fe=C.children;for(let re=0;re{o(N)}:yn}function o(p){if(ko(p)){const g=s.get(p);g&&(s.delete(p),n.splice(n.indexOf(g),1),g.children.forEach(o),g.alias.forEach(o))}else{const g=n.indexOf(p);g>-1&&(n.splice(g,1),p.record.name&&s.delete(p.record.name),p.children.forEach(o),p.alias.forEach(o))}}function a(){return n}function l(p){const g=sc(p,n);n.splice(g,0,p),p.record.name&&!rr(p)&&s.set(p.record.name,p)}function u(p,g){let v,T={},C,y;if("name"in p&&p.name){if(v=s.get(p.name),!v)throw nn(de.MATCHER_NOT_FOUND,{location:p});y=v.record.name,T=te(sr(g.params,v.keys.filter(N=>!N.optional).concat(v.parent?v.parent.keys.filter(N=>N.optional):[]).map(N=>N.name)),p.params&&sr(p.params,v.keys.map(N=>N.name))),C=v.stringify(T)}else if(p.path!=null)C=p.path,v=n.find(N=>N.re.test(C)),v&&(T=v.parse(C),y=v.record.name);else{if(v=g.name?s.get(g.name):n.find(N=>N.re.test(g.path)),!v)throw nn(de.MATCHER_NOT_FOUND,{location:p,currentLocation:g});y=v.record.name,T=te({},g.params,p.params),C=v.stringify(T)}const $=[];let E=v;for(;E;)$.unshift(E.record),E=E.parent;return{name:y,path:C,params:T,matched:$,meta:nc($)}}e.forEach(p=>r(p));function c(){n.length=0,s.clear()}return{addRoute:r,resolve:u,removeRoute:o,clearRoutes:c,getRoutes:a,getRecordMatcher:i}}function sr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function ir(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:tc(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function tc(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function rr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function nc(e){return e.reduce((t,n)=>te(t,n.meta),{})}function sc(e,t){let n=0,s=t.length;for(;n!==s;){const r=n+s>>1;No(e,t[r])<0?s=r:n=r+1}const i=ic(e);return i&&(s=t.lastIndexOf(i,s-1)),s}function ic(e){let t=e;for(;t=t.parent;)if(Lo(t)&&No(e,t)===0)return t}function Lo({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function or(e){const t=Ye(ps),n=Ye(fi),s=_e(()=>{const l=he(e.to);return t.resolve(l)}),i=_e(()=>{const{matched:l}=s.value,{length:u}=l,c=l[u-1],p=n.matched;if(!c||!p.length)return-1;const g=p.findIndex(tn.bind(null,c));if(g>-1)return g;const v=lr(l[u-2]);return u>1&&lr(c)===v&&p[p.length-1].path!==v?p.findIndex(tn.bind(null,l[u-2])):g}),r=_e(()=>i.value>-1&&uc(n.params,s.value.params)),o=_e(()=>i.value>-1&&i.value===n.matched.length-1&&Po(n.params,s.value.params));function a(l={}){if(ac(l)){const u=t[he(e.replace)?"replace":"push"](he(e.to)).catch(yn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>u),u}return Promise.resolve()}return{route:s,href:_e(()=>s.value.href),isActive:r,isExactActive:o,navigate:a}}function rc(e){return e.length===1?e[0]:e}const oc=we({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:or,setup(e,{slots:t}){const n=Mn(or(e)),{options:s}=Ye(ps),i=_e(()=>({[ar(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[ar(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const r=t.default&&rc(t.default(n));return e.custom?r:wo("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},r)}}}),lc=oc;function ac(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function uc(e,t){for(const n in t){const s=t[n],i=e[n];if(typeof s=="string"){if(s!==i)return!1}else if(!Ze(i)||i.length!==s.length||s.some((r,o)=>r.valueOf()!==i[o].valueOf()))return!1}return!0}function lr(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const ar=(e,t,n)=>e??t??n,cc=we({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=Ye(zs),i=_e(()=>e.route||s.value),r=Ye(Zi,0),o=_e(()=>{let u=he(r);const{matched:c}=i.value;let p;for(;(p=c[u])&&!p.components;)u++;return u}),a=_e(()=>i.value.matched[o.value]);hn(Zi,_e(()=>o.value+1)),hn(Vu,a),hn(zs,i);const l=Oe();return Yt(()=>[l.value,a.value,e.name],([u,c,p],[g,v,T])=>{c&&(c.instances[p]=u,v&&v!==c&&u&&u===g&&(c.leaveGuards.size||(c.leaveGuards=v.leaveGuards),c.updateGuards.size||(c.updateGuards=v.updateGuards))),u&&c&&(!v||!tn(c,v)||!g)&&(c.enterCallbacks[p]||[]).forEach(C=>C(u))},{flush:"post"}),()=>{const u=i.value,c=e.name,p=a.value,g=p&&p.components[c];if(!g)return ur(n.default,{Component:g,route:u});const v=p.props[c],T=v?v===!0?u.params:typeof v=="function"?v(u):v:null,y=wo(g,te({},T,t,{onVnodeUnmounted:$=>{$.component.isUnmounted&&(p.instances[c]=null)},ref:l}));return ur(n.default,{Component:y,route:u})||y}}});function ur(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const fc=cc;function dc(e){const t=ec(e.routes,e),n=e.parseQuery||Du,s=e.stringifyQuery||Xi,i=e.history,r=ln(),o=ln(),a=ln(),l=gl(Rt);let u=Rt;Kt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const c=Ss.bind(null,w=>""+w),p=Ss.bind(null,bu),g=Ss.bind(null,Cn);function v(w,H){let O,F;return ko(w)?(O=t.getRecordMatcher(w),F=H):F=w,t.addRoute(F,O)}function T(w){const H=t.getRecordMatcher(w);H&&t.removeRoute(H)}function C(){return t.getRoutes().map(w=>w.record)}function y(w){return!!t.getRecordMatcher(w)}function $(w,H){if(H=te({},H||l.value),typeof w=="string"){const m=Cs(n,w,H.path),_=t.resolve({path:m.path},H),x=i.createHref(m.fullPath);return te(m,_,{params:g(_.params),hash:Cn(m.hash),redirectedFrom:void 0,href:x})}let O;if(w.path!=null)O=te({},w,{path:Cs(n,w.path,H.path).path});else{const m=te({},w.params);for(const _ in m)m[_]==null&&delete m[_];O=te({},w,{params:p(m)}),H.params=p(H.params)}const F=t.resolve(O,H),Y=w.hash||"";F.params=c(g(F.params));const f=xu(s,te({},w,{hash:gu(Y),path:F.path})),d=i.createHref(f);return te({fullPath:f,hash:Y,query:s===Xi?Hu(w.query):w.query||{}},F,{redirectedFrom:void 0,href:d})}function E(w){return typeof w=="string"?Cs(n,w,l.value.path):te({},w)}function N(w,H){if(u!==w)return nn(de.NAVIGATION_CANCELLED,{from:H,to:w})}function V(w){return re(w)}function Z(w){return V(te(E(w),{replace:!0}))}function fe(w,H){const O=w.matched[w.matched.length-1];if(O&&O.redirect){const{redirect:F}=O;let Y=typeof F=="function"?F(w,H):F;return typeof Y=="string"&&(Y=Y.includes("?")||Y.includes("#")?Y=E(Y):{path:Y},Y.params={}),te({query:w.query,hash:w.hash,params:Y.path!=null?{}:w.params},Y)}}function re(w,H){const O=u=$(w),F=l.value,Y=w.state,f=w.force,d=w.replace===!0,m=fe(O,F);if(m)return re(te(E(m),{state:typeof m=="object"?te({},Y,m.state):Y,force:f,replace:d}),H||O);const _=O;_.redirectedFrom=H;let x;return!f&&Eu(s,F,O)&&(x=nn(de.NAVIGATION_DUPLICATED,{to:_,from:F}),je(F,F,!0,!1)),(x?Promise.resolve(x):D(_,F)).catch(b=>ht(b)?ht(b,de.NAVIGATION_GUARD_REDIRECT)?b:tt(b):X(b,_,F)).then(b=>{if(b){if(ht(b,de.NAVIGATION_GUARD_REDIRECT))return re(te({replace:d},E(b.to),{state:typeof b.to=="object"?te({},Y,b.to.state):Y,force:f}),H||_)}else b=et(_,F,!0,d,Y);return q(_,F,b),b})}function Ue(w,H){const O=N(w,H);return O?Promise.reject(O):Promise.resolve()}function z(w){const H=At.values().next().value;return H&&typeof H.runWithContext=="function"?H.runWithContext(w):w()}function D(w,H){let O;const[F,Y,f]=$u(w,H);O=Rs(F.reverse(),"beforeRouteLeave",w,H);for(const m of F)m.leaveGuards.forEach(_=>{O.push(Tt(_,w,H))});const d=Ue.bind(null,w,H);return O.push(d),Ge(O).then(()=>{O=[];for(const m of r.list())O.push(Tt(m,w,H));return O.push(d),Ge(O)}).then(()=>{O=Rs(Y,"beforeRouteUpdate",w,H);for(const m of Y)m.updateGuards.forEach(_=>{O.push(Tt(_,w,H))});return O.push(d),Ge(O)}).then(()=>{O=[];for(const m of f)if(m.beforeEnter)if(Ze(m.beforeEnter))for(const _ of m.beforeEnter)O.push(Tt(_,w,H));else O.push(Tt(m.beforeEnter,w,H));return O.push(d),Ge(O)}).then(()=>(w.matched.forEach(m=>m.enterCallbacks={}),O=Rs(f,"beforeRouteEnter",w,H,z),O.push(d),Ge(O))).then(()=>{O=[];for(const m of o.list())O.push(Tt(m,w,H));return O.push(d),Ge(O)}).catch(m=>ht(m,de.NAVIGATION_CANCELLED)?m:Promise.reject(m))}function q(w,H,O){a.list().forEach(F=>z(()=>F(w,H,O)))}function et(w,H,O,F,Y){const f=N(w,H);if(f)return f;const d=H===Rt,m=Kt?history.state:{};O&&(F||d?i.replace(w.fullPath,te({scroll:d&&m&&m.scroll},Y)):i.push(w.fullPath,Y)),l.value=w,je(w,H,O,d),tt()}let Le;function Pt(){Le||(Le=i.listen((w,H,O)=>{if(!kt.listening)return;const F=$(w),Y=fe(F,kt.currentRoute.value);if(Y){re(te(Y,{replace:!0,force:!0}),F).catch(yn);return}u=F;const f=l.value;Kt&&ku(Qi(f.fullPath,O.delta),ds()),D(F,f).catch(d=>ht(d,de.NAVIGATION_ABORTED|de.NAVIGATION_CANCELLED)?d:ht(d,de.NAVIGATION_GUARD_REDIRECT)?(re(te(E(d.to),{force:!0}),F).then(m=>{ht(m,de.NAVIGATION_ABORTED|de.NAVIGATION_DUPLICATED)&&!O.delta&&O.type===Gs.pop&&i.go(-1,!1)}).catch(yn),Promise.reject()):(O.delta&&i.go(-O.delta,!1),X(d,F,f))).then(d=>{d=d||et(F,f,!1),d&&(O.delta&&!ht(d,de.NAVIGATION_CANCELLED)?i.go(-O.delta,!1):O.type===Gs.pop&&ht(d,de.NAVIGATION_ABORTED|de.NAVIGATION_DUPLICATED)&&i.go(-1,!1)),q(F,f,d)}).catch(yn)}))}let St=ln(),ge=ln(),ee;function X(w,H,O){tt(w);const F=ge.list();return F.length?F.forEach(Y=>Y(w,H,O)):console.error(w),Promise.reject(w)}function ze(){return ee&&l.value!==Rt?Promise.resolve():new Promise((w,H)=>{St.add([w,H])})}function tt(w){return ee||(ee=!w,Pt(),St.list().forEach(([H,O])=>w?O(w):H()),St.reset()),w}function je(w,H,O,F){const{scrollBehavior:Y}=e;if(!Kt||!Y)return Promise.resolve();const f=!O&&Ou(Qi(w.fullPath,0))||(F||!O)&&history.state&&history.state.scroll||null;return is().then(()=>Y(w,H,f)).then(d=>d&&Pu(d)).catch(d=>X(d,w,H))}const Ae=w=>i.go(w);let Ct;const At=new Set,kt={currentRoute:l,listening:!0,addRoute:v,removeRoute:T,clearRoutes:t.clearRoutes,hasRoute:y,getRoutes:C,resolve:$,options:e,push:V,replace:Z,go:Ae,back:()=>Ae(-1),forward:()=>Ae(1),beforeEach:r.add,beforeResolve:o.add,afterEach:a.add,onError:ge.add,isReady:ze,install(w){w.component("RouterLink",lc),w.component("RouterView",fc),w.config.globalProperties.$router=kt,Object.defineProperty(w.config.globalProperties,"$route",{enumerable:!0,get:()=>he(l)}),Kt&&!Ct&&l.value===Rt&&(Ct=!0,V(i.location).catch(F=>{}));const H={};for(const F in Rt)Object.defineProperty(H,F,{get:()=>l.value[F],enumerable:!0});w.provide(ps,kt),w.provide(fi,Nr(H)),w.provide(zs,l);const O=w.unmount;At.add(w),w.unmount=function(){At.delete(w),At.size<1&&(u=Rt,Le&&Le(),Le=null,l.value=Rt,Ct=!1,ee=!1),O()}}};function Ge(w){return w.reduce((H,O)=>H.then(()=>z(O)),Promise.resolve())}return kt}function Do(){return Ye(ps)}function pc(e){return Ye(fi)}const hc=["width","height"],mc={key:8,d:"M6 4.5v15l13-7.5z",fill:"currentColor",stroke:"none"},gc={key:17,d:"m9 6 6 6-6 6"},vc={key:18,d:"m6 9 6 6 6-6"},yc={key:19,d:"m15 6-6 6 6 6"},bc={key:23,d:"M4 12.5 9 17 20 6"},_c={key:28,x:"5",y:"5",width:"14",height:"14",rx:"2",fill:"currentColor",stroke:"none"},wc={key:34,d:"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"},me=we({__name:"Icon",props:{name:{},size:{}},setup(e){return(t,n)=>(A(),k("svg",{width:e.size??16,height:e.size??16,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"ico"},[e.name==="dashboard"?(A(),k(j,{key:0},[n[0]||(n[0]=h("rect",{x:"3",y:"3",width:"7",height:"9",rx:"1.5"},null,-1)),n[1]||(n[1]=h("rect",{x:"14",y:"3",width:"7",height:"5",rx:"1.5"},null,-1)),n[2]||(n[2]=h("rect",{x:"14",y:"12",width:"7",height:"9",rx:"1.5"},null,-1)),n[3]||(n[3]=h("rect",{x:"3",y:"16",width:"7",height:"5",rx:"1.5"},null,-1))],64)):e.name==="playlist"?(A(),k(j,{key:1},[n[4]||(n[4]=Lt('',6))],64)):e.name==="epg"?(A(),k(j,{key:2},[n[5]||(n[5]=Lt('',7))],64)):e.name==="import"?(A(),k(j,{key:3},[n[6]||(n[6]=h("path",{d:"M12 3v12"},null,-1)),n[7]||(n[7]=h("path",{d:"m7 10 5 5 5-5"},null,-1)),n[8]||(n[8]=h("path",{d:"M5 21h14"},null,-1))],64)):e.name==="map"?(A(),k(j,{key:4},[n[9]||(n[9]=h("circle",{cx:"6",cy:"6",r:"2.5"},null,-1)),n[10]||(n[10]=h("circle",{cx:"18",cy:"18",r:"2.5"},null,-1)),n[11]||(n[11]=h("path",{d:"M8.5 6h7a3 3 0 0 1 0 6h-7a3 3 0 0 0 0 6h-2"},null,-1))],64)):e.name==="settings"?(A(),k(j,{key:5},[n[12]||(n[12]=h("circle",{cx:"12",cy:"12",r:"3"},null,-1)),n[13]||(n[13]=h("path",{d:"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"},null,-1))],64)):e.name==="search"?(A(),k(j,{key:6},[n[14]||(n[14]=h("circle",{cx:"11",cy:"11",r:"7"},null,-1)),n[15]||(n[15]=h("path",{d:"m21 21-4.3-4.3"},null,-1))],64)):e.name==="plus"?(A(),k(j,{key:7},[n[16]||(n[16]=h("path",{d:"M12 5v14"},null,-1)),n[17]||(n[17]=h("path",{d:"M5 12h14"},null,-1))],64)):e.name==="play"?(A(),k("path",mc)):e.name==="pause"?(A(),k(j,{key:9},[n[18]||(n[18]=h("rect",{x:"6",y:"5",width:"4",height:"14"},null,-1)),n[19]||(n[19]=h("rect",{x:"14",y:"5",width:"4",height:"14"},null,-1))],64)):e.name==="refresh"?(A(),k(j,{key:10},[n[20]||(n[20]=h("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"},null,-1)),n[21]||(n[21]=h("path",{d:"M21 3v5h-5"},null,-1)),n[22]||(n[22]=h("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"},null,-1)),n[23]||(n[23]=h("path",{d:"M3 21v-5h5"},null,-1))],64)):e.name==="trash"?(A(),k(j,{key:11},[n[24]||(n[24]=Lt('',5))],64)):e.name==="edit"?(A(),k(j,{key:12},[n[25]||(n[25]=h("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"},null,-1)),n[26]||(n[26]=h("path",{d:"m18.5 2.5 3 3L12 15l-4 1 1-4z"},null,-1))],64)):e.name==="more"?(A(),k(j,{key:13},[n[27]||(n[27]=h("circle",{cx:"5",cy:"12",r:"1.4"},null,-1)),n[28]||(n[28]=h("circle",{cx:"12",cy:"12",r:"1.4"},null,-1)),n[29]||(n[29]=h("circle",{cx:"19",cy:"12",r:"1.4"},null,-1))],64)):e.name==="grid"?(A(),k(j,{key:14},[n[30]||(n[30]=h("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"},null,-1)),n[31]||(n[31]=h("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"},null,-1)),n[32]||(n[32]=h("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"},null,-1)),n[33]||(n[33]=h("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"},null,-1))],64)):e.name==="list"?(A(),k(j,{key:15},[n[34]||(n[34]=Lt('',6))],64)):e.name==="filter"?(A(),k(j,{key:16},[n[35]||(n[35]=h("path",{d:"M3 5h18"},null,-1)),n[36]||(n[36]=h("path",{d:"M6 12h12"},null,-1)),n[37]||(n[37]=h("path",{d:"M10 19h4"},null,-1))],64)):e.name==="chevron-r"?(A(),k("path",gc)):e.name==="chevron-d"?(A(),k("path",vc)):e.name==="chevron-l"?(A(),k("path",yc)):e.name==="upload"?(A(),k(j,{key:20},[n[38]||(n[38]=h("path",{d:"M12 15V3"},null,-1)),n[39]||(n[39]=h("path",{d:"m7 8 5-5 5 5"},null,-1)),n[40]||(n[40]=h("path",{d:"M5 21h14"},null,-1))],64)):e.name==="link"?(A(),k(j,{key:21},[n[41]||(n[41]=h("path",{d:"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"},null,-1)),n[42]||(n[42]=h("path",{d:"M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"},null,-1))],64)):e.name==="tv"?(A(),k(j,{key:22},[n[43]||(n[43]=h("rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"},null,-1)),n[44]||(n[44]=h("path",{d:"M8 22h8"},null,-1)),n[45]||(n[45]=h("path",{d:"M12 19v3"},null,-1))],64)):e.name==="check"?(A(),k("path",bc)):e.name==="x"?(A(),k(j,{key:24},[n[46]||(n[46]=h("path",{d:"M18 6 6 18"},null,-1)),n[47]||(n[47]=h("path",{d:"m6 6 12 12"},null,-1))],64)):e.name==="sync"?(A(),k(j,{key:25},[n[48]||(n[48]=h("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"},null,-1)),n[49]||(n[49]=h("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"},null,-1))],64)):e.name==="warn"?(A(),k(j,{key:26},[n[50]||(n[50]=h("path",{d:"M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.41 0z"},null,-1)),n[51]||(n[51]=h("path",{d:"M12 9v4"},null,-1)),n[52]||(n[52]=h("circle",{cx:"12",cy:"17",r:"0.6",fill:"currentColor"},null,-1))],64)):e.name==="add"?(A(),k(j,{key:27},[n[53]||(n[53]=h("circle",{cx:"12",cy:"12",r:"9"},null,-1)),n[54]||(n[54]=h("path",{d:"M12 8v8"},null,-1)),n[55]||(n[55]=h("path",{d:"M8 12h8"},null,-1))],64)):e.name==="stop"?(A(),k("rect",_c)):e.name==="globe"?(A(),k(j,{key:29},[n[56]||(n[56]=h("circle",{cx:"12",cy:"12",r:"9"},null,-1)),n[57]||(n[57]=h("path",{d:"M3 12h18"},null,-1)),n[58]||(n[58]=h("path",{d:"M12 3a14 14 0 0 1 0 18"},null,-1)),n[59]||(n[59]=h("path",{d:"M12 3a14 14 0 0 0 0 18"},null,-1))],64)):e.name==="file"?(A(),k(j,{key:30},[n[60]||(n[60]=h("path",{d:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"},null,-1)),n[61]||(n[61]=h("path",{d:"M14 2v6h6"},null,-1))],64)):e.name==="logout"?(A(),k(j,{key:31},[n[62]||(n[62]=h("path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"},null,-1)),n[63]||(n[63]=h("path",{d:"m16 17 5-5-5-5"},null,-1)),n[64]||(n[64]=h("path",{d:"M21 12H9"},null,-1))],64)):e.name==="copy"?(A(),k(j,{key:32},[n[65]||(n[65]=h("rect",{x:"8",y:"8",width:"12",height:"12",rx:"2"},null,-1)),n[66]||(n[66]=h("path",{d:"M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"},null,-1))],64)):e.name==="sun"?(A(),k(j,{key:33},[n[67]||(n[67]=Lt('',9))],64)):e.name==="moon"?(A(),k("path",wc)):He("",!0)],8,hc))}}),xc=["title","disabled"],$e=we({__name:"Btn",props:{variant:{},size:{},icon:{},title:{},disabled:{type:Boolean}},setup(e){const t=e,n=Kl(),s=_e(()=>{const i=!!n.default;return["btn",t.variant,t.size,t.icon&&!i?"icon":""].filter(Boolean).join(" ")});return(i,r)=>(A(),k("button",{class:Be(s.value),title:e.title,disabled:e.disabled},[e.icon?(A(),We(me,{key:0,name:e.icon,size:e.size==="sm"?13:14},null,8,["name","size"])):He("",!0),as(i.$slots,"default")],10,xc))}}),Ec={class:"twk-body"},jt=16,Sc=we({__name:"TweaksPanel",props:{title:{}},setup(e){const t=Oe(!1),n=Oe(null),s=Oe({x:16,y:16});function i(){const l=n.value;if(!l)return;const u=l.offsetWidth,c=l.offsetHeight,p=Math.max(jt,window.innerWidth-u-jt),g=Math.max(jt,window.innerHeight-c-jt);s.value={x:Math.min(p,Math.max(jt,s.value.x)),y:Math.min(g,Math.max(jt,s.value.y))}}function r(l){var c;const u=(c=l==null?void 0:l.data)==null?void 0:c.type;u==="__activate_edit_mode"?t.value=!0:u==="__deactivate_edit_mode"&&(t.value=!1)}function o(){var l;t.value=!1;try{(l=window.parent)==null||l.postMessage({type:"__edit_mode_dismissed"},"*")}catch{}}function a(l){const u=n.value;if(!u)return;const c=u.getBoundingClientRect(),p=l.clientX,g=l.clientY,v=window.innerWidth-c.right,T=window.innerHeight-c.bottom,C=$=>{s.value={x:v-($.clientX-p),y:T-($.clientY-g)},i()},y=()=>{window.removeEventListener("mousemove",C),window.removeEventListener("mouseup",y)};window.addEventListener("mousemove",C),window.addEventListener("mouseup",y)}return os(()=>{var l;window.addEventListener("message",r);try{(l=window.parent)==null||l.postMessage({type:"__edit_mode_available"},"*")}catch{}window.addEventListener("resize",i)}),ls(()=>{window.removeEventListener("message",r),window.removeEventListener("resize",i)}),(l,u)=>t.value?(A(),k("div",{key:0,ref_key:"panel",ref:n,class:"twk-panel",style:dt({right:s.value.x+"px",bottom:s.value.y+"px"})},[h("div",{class:"twk-hd",onMousedown:a},[h("b",null,J(e.title||"Tweaks"),1),h("button",{class:"twk-x","aria-label":"Close tweaks",onMousedown:u[0]||(u[0]=Eo(()=>{},["stop"])),onClick:o},"✕",32)],32),h("div",Ec,[as(l.$slots,"default")])],4)):He("",!0)}}),Cc={class:"twk-sect"},Ms=we({__name:"TweakSection",props:{label:{}},setup(e){return(t,n)=>(A(),k("div",Cc,J(e.label),1))}}),Ac={class:"twk-row"},Rc={class:"twk-lbl"},Mc={class:"twk-seg",role:"radiogroup"},Tc=["aria-checked","onClick"],Ts=we({__name:"TweakRadio",props:{label:{},value:{},options:{}},emits:["change"],setup(e,{emit:t}){const n=e,s=t,i=_e(()=>Math.max(0,n.options.indexOf(n.value))),r=_e(()=>n.options.length);return(o,a)=>(A(),k("div",Ac,[h("div",Rc,[h("span",null,J(e.label),1)]),h("div",Mc,[h("div",{class:"twk-seg-thumb",style:dt({left:`calc(2px + ${i.value} * (100% - 4px) / ${r.value})`,width:`calc((100% - 4px) / ${r.value})`})},null,4),(A(!0),k(j,null,Ft(e.options,l=>(A(),k("button",{key:l,type:"button",role:"radio","aria-checked":l===e.value,onClick:u=>s("change",l)},J(l),9,Tc))),128))])]))}}),cn=we({__name:"Pill",props:{tone:{}},setup(e){return(t,n)=>(A(),k("span",{class:Be(["pill",e.tone||""])},[as(t.$slots,"default")],2))}}),Pc=["title"],kc=we({__name:"StatusDot",props:{status:{},pulse:{type:Boolean}},setup(e){return(t,n)=>(A(),k("div",{class:Be(["dot",e.status||"idle",{pulse:e.pulse}]),title:e.status},null,10,Pc))}}),Oc=we({__name:"ChannelLogo",props:{ch:{},size:{}},setup(e){return(t,n)=>(A(),k("div",{class:Be(["ch-logo",e.size||""]),style:dt({background:e.ch.logoColor+" linear-gradient(135deg, transparent, rgba(0,0,0,.25))",color:"white"})},J(e.ch.initials),7))}}),Ic={class:"segmented"},Nc=["onClick"],Ho=we({__name:"Segmented",props:{value:{},options:{}},emits:["change"],setup(e,{emit:t}){const n=e,s=t;return(i,r)=>(A(),k("div",Ic,[(A(!0),k(j,null,Ft(n.options,o=>(A(),k("button",{key:o.value,class:Be(e.value===o.value?"active":""),onClick:a=>s("change",o.value)},[o.icon?(A(),We(me,{key:0,name:o.icon,size:13},null,8,["name"])):He("",!0),pe(" "+J(o.label),1)],10,Nc))),128))]))}}),Nn=[{id:"pl-default",name:"Default",url:"bundled://tvapp2/default.m3u",channels:24,groups:6,lastSync:"Ships with TVApp2",status:"good",auto:!0,interval:"Auto-updated",builtin:!0},{id:"pl-iptv-pro",name:"IPTV-Pro Main",url:"https://iptv-pro.example.com/playlist.m3u8",channels:142,groups:8,lastSync:"2 minutes ago",status:"good",auto:!0,interval:"Every 6 hours"},{id:"pl-free-uk",name:"Free UK Bouquet",url:"https://iptv-org.github.io/iptv/countries/uk.m3u",channels:87,groups:4,lastSync:"1 hour ago",status:"good",auto:!0,interval:"Daily"},{id:"pl-archive",name:"Archive (legacy)",url:"file:///playlists/archive-2023.m3u",channels:38,groups:3,lastSync:"3 days ago",status:"warn",auto:!1,interval:"Manual"}],Ln=[{id:"epg-default",name:"Default",url:"bundled://tvapp2/default.xml.gz",channels:86,programs:5240,lastSync:"Ships with TVApp2",status:"good",auto:!0,interval:"Auto-updated",builtin:!0},{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:!0,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:!0,interval:"Daily"}],Lc=["News","Sport","Entertainment","Movies","Kids","Music","Documentary","Lifestyle"];function Dc(e){let t=e;return()=>(t=t*1664525+1013904223>>>0,t/4294967296)}const Hc=[{tvg_name:"BBC One HD",group:"Entertainment",channel:101,tvg_id:"bbc.one.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"BBC Two HD",group:"Entertainment",channel:102,tvg_id:"bbc.two.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"BBC News",group:"News",channel:231,tvg_id:"bbc.news.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Sky Sports Main",group:"Sport",channel:401,tvg_id:"sky.sports.main.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Sky Sports F1",group:"Sport",channel:406,tvg_id:"sky.sports.f1.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"ITV1 HD",group:"Entertainment",channel:103,tvg_id:"itv1.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Channel 4 HD",group:"Entertainment",channel:104,tvg_id:"channel4.uk",state:"active",epg:"matched",status:"warn",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Film4",group:"Movies",channel:315,tvg_id:"film4.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Discovery Channel",group:"Documentary",channel:520,tvg_id:"discovery.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"National Geographic",group:"Documentary",channel:521,tvg_id:"natgeo.uk",state:"active",epg:"unmatched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"CNN International",group:"News",channel:233,tvg_id:"cnn.int",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Al Jazeera English",group:"News",channel:235,tvg_id:"aljazeera.en",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Cartoon Network",group:"Kids",channel:601,tvg_id:"cartoonnet.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Nick Jr.",group:"Kids",channel:615,tvg_id:null,state:"disabled",epg:"unmatched",status:"warn",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"MTV Hits",group:"Music",channel:365,tvg_id:"mtv.hits.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Kerrang!",group:"Music",channel:369,tvg_id:"kerrang.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Food Network",group:"Lifestyle",channel:240,tvg_id:"foodnet.uk",state:"active",epg:"matched",status:"good",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"HGTV",group:"Lifestyle",channel:242,tvg_id:null,state:"disabled",epg:"unmatched",status:"bad",res:"720p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"TCM Movies",group:"Movies",channel:320,tvg_id:"tcm.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"},{tvg_name:"Eurosport 1",group:"Sport",channel:410,tvg_id:"eurosport1.uk",state:"active",epg:"matched",status:"good",res:"1080p",url:"http://sample.stream.com/channel/1.m3u8"}],Ws=Hc.map((e,t)=>({id:`ch-${t}`,...e,source:"Default",logoColor:`oklch(0.5 0.16 ${t*47%360})`,initials:e.tvg_name.split(/\s+/).slice(0,2).map(n=>n[0]).join("").toUpperCase()})),Ps=[["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"]],Td=Array.from({length:25},(e,t)=>t);function Vc(e,t){const n=Dc(t),s=[];let i=0;for(;i<24;){const r=[.5,1,1,1.5,2][Math.floor(n()*5)],o=Math.floor(n()*Ps.length);s.push({start:i,end:Math.min(24,i+r),title:Ps[o][0],cat:Ps[o][1]}),i+=r}return s}const $c={};Ws.slice(0,12).forEach((e,t)=>{$c[e.id]=Vc(e.id,100+t*7)});const Pd=[{when:"2m",icon:"sync",html:"IPTV-Pro Main synced — 142 channels, no changes"},{when:"12m",icon:"epg",html:"XMLTV UK Guide imported — 8,420 programs across 124 channels"},{when:"1h",icon:"map",html:'Manual mapping: HGTVhgtv.uk'},{when:"1h",icon:"warn",html:"Free UK Bouquet reports 3 channels offline (HTTP 503)"},{when:"3h",icon:"edit",html:"Renamed DiscoveryDiscovery Channel"},{when:"Yest.",icon:"add",html:"Playlist IPTV-Pro Main added (142 channels)"}],cr=[{id:"as-1",channelId:"ch-0",status:"good",uptime:"4h 12m",uptimeMin:252,viewers:142,peakViewers:168,bitrate:6.4,targetBitrate:6,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,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,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:.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,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,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,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:.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,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,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,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,latency:2.2,bandwidth:68},{id:"as-7",channelId:"ch-17",status:"bad",uptime:"—",uptimeMin:0,viewers:0,peakViewers:4,bitrate:0,targetBitrate:5,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}],kd=[{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"}];function Od(e,t){let n=e;const s=()=>(n=n*1664525+1013904223>>>0,n/4294967296),i=[];for(let r=0;r<60;r++){const o=(s()-.5)*.6;i.push(Math.max(.2,t+o))}return i}const Id=[{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"}],Fc={class:"drawer-wrap"},Bc={class:"glass drawer-panel"},Uc={class:"drawer-hd"},jc={style:{flex:"1"}},Gc={style:{"font-weight":"600","font-size":"15px"}},Kc={class:"mono muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},zc={class:"drawer-body"},Wc={class:"player"},qc={class:"label mono"},Yc={class:"play"},Jc={class:"play-btn"},Qc={class:"controls"},Xc={class:"row",style:{gap:"6px"}},Zc={class:"form-row"},ef={class:"row",style:{gap:"10px"}},tf={class:"form-row"},nf={class:"input"},sf=["value"],rf={class:"form-grid-2"},of={class:"form-row"},lf={class:"input"},af=["value"],uf={class:"form-row"},cf={class:"select"},ff=["value"],df={class:"form-row"},pf={class:"input"},hf=["value"],mf={class:"form-row"},gf={class:"input"},vf=["value"],yf={class:"form-row"},bf={class:"input mono",style:{"font-size":"11px"}},_f=["value"],wf={class:"row",style:{"margin-top":"6px"}},xf=we({__name:"ChannelDrawer",props:{ch:{}},emits:["close"],setup(e,{emit:t}){const n=e,s=t;function i(r){n.ch.state=r}return(r,o)=>(A(),k("div",Fc,[h("div",{class:"glass-bg drawer-backdrop",onClick:o[0]||(o[0]=a=>s("close"))}),h("div",Bc,[h("div",Uc,[L(Oc,{ch:e.ch,size:"lg"},null,8,["ch"]),h("div",jc,[h("div",Gc,J(e.ch.tvg_name),1),h("div",Kc," #"+J(e.ch.channel)+" · "+J(e.ch.group)+" · "+J(e.ch.res),1)]),L($e,{variant:"ghost",size:"sm",icon:"x",onClick:o[1]||(o[1]=a=>s("close"))})]),h("div",zc,[h("div",Wc,[o[7]||(o[7]=h("div",{class:"stripes"},null,-1)),h("div",qc,"STREAM TEST · "+J(e.ch.res),1),h("div",Yc,[h("div",Jc,[L(me,{name:"play",size:26})])]),h("div",Qc,[L(me,{name:"pause",size:14}),o[4]||(o[4]=h("span",{class:"mono",style:{"font-size":"11px"}},"00:14",-1)),o[5]||(o[5]=h("div",{class:"track"},null,-1)),o[6]||(o[6]=h("span",{class:"mono",style:{"font-size":"11px"}},"LIVE",-1))])]),h("div",Xc,[L(cn,{tone:"good"},{default:be(()=>[L(kc,{status:"good"}),o[8]||(o[8]=pe(" stream live",-1))]),_:1}),L(cn,{tone:"cyan"},{default:be(()=>[L(me,{name:"globe",size:11}),o[9]||(o[9]=pe(" 1280×720",-1))]),_:1}),L(cn,null,{default:be(()=>[...o[10]||(o[10]=[pe("h.264 / AAC",-1)])]),_:1}),o[12]||(o[12]=h("span",{class:"spacer"},null,-1)),L($e,{variant:"ghost",size:"sm",icon:"refresh"},{default:be(()=>[...o[11]||(o[11]=[pe("Re-test",-1)])]),_:1})]),o[24]||(o[24]=h("div",{class:"divider"},null,-1)),h("div",Zc,[o[13]||(o[13]=h("div",{class:"field-lbl"},"State",-1)),h("div",ef,[L(Ho,{value:e.ch.state,onChange:i,options:[{value:"active",label:"Active",icon:"check"},{value:"disabled",label:"Disabled",icon:"x"}]},null,8,["value"]),L(cn,{tone:e.ch.state==="active"?"active":"disabled"},{default:be(()=>[pe(J(e.ch.state==="active"?"active":"disabled"),1)]),_:1},8,["tone"])])]),h("div",tf,[o[14]||(o[14]=h("div",{class:"field-lbl"},"Display name",-1)),h("div",nf,[h("input",{value:e.ch.tvg_name},null,8,sf)])]),h("div",rf,[h("div",of,[o[15]||(o[15]=h("div",{class:"field-lbl"},"Channel number",-1)),h("div",lf,[h("input",{value:e.ch.channel},null,8,af)])]),h("div",uf,[o[16]||(o[16]=h("div",{class:"field-lbl"},"Group",-1)),h("div",cf,[h("select",{value:e.ch.group},[(A(!0),k(j,null,Ft(he(Lc),a=>(A(),k("option",{key:a},J(a),1))),128))],8,ff)])])]),h("div",df,[o[17]||(o[17]=h("div",{class:"field-lbl"},"Source",-1)),h("div",pf,[L(me,{name:"playlist",size:14}),h("input",{value:e.ch.source,placeholder:"e.g. Default"},null,8,hf)])]),h("div",mf,[o[18]||(o[18]=h("div",{class:"field-lbl"},"TVG-ID (EPG link)",-1)),h("div",gf,[L(me,{name:"link",size:14}),h("input",{value:e.ch.tvg_id||"",placeholder:"e.g. bbc.one.uk"},null,8,vf)])]),h("div",yf,[o[19]||(o[19]=h("div",{class:"field-lbl"},"URL",-1)),h("div",bf,[L(me,{name:"link",size:14}),h("input",{value:e.ch.url,placeholder:"https://example.com/live/channel/index.m3u8"},null,8,_f)])]),h("div",wf,[L($e,{variant:"ghost",icon:"trash"},{default:be(()=>[...o[20]||(o[20]=[h("span",{style:{color:"var(--bad)"}},"Remove",-1)])]),_:1}),o[23]||(o[23]=h("span",{class:"spacer"},null,-1)),L($e,{variant:"ghost",onClick:o[2]||(o[2]=a=>s("close"))},{default:be(()=>[...o[21]||(o[21]=[pe("Cancel",-1)])]),_:1}),L($e,{variant:"primary",icon:"check",onClick:o[3]||(o[3]=a=>s("close"))},{default:be(()=>[...o[22]||(o[22]=[pe("Save changes",-1)])]),_:1})])])])]))}}),Ef=["aria-checked"],Sf=we({__name:"Toggle",props:{on:{type:Boolean}},emits:["change"],setup(e,{emit:t}){const n=e,s=t;return(i,r)=>(A(),k("div",{class:Be(["toggle",{on:n.on}]),role:"switch","aria-checked":!!n.on,onClick:r[0]||(r[0]=o=>s("change",!n.on))},null,10,Ef))}}),Cf={class:"row",style:{"align-items":"center",padding:"10px 0"}},Af={style:{flex:"1"}},Rf={style:{"font-weight":"500","font-size":"var(--fs-base)"}},Mf={class:"muted",style:{"font-size":"var(--fs-xs)","margin-top":"2px"}},Tf=we({__name:"SettingsRow",props:{label:{},hint:{}},setup(e){return(t,n)=>(A(),k("div",Cf,[h("div",Af,[h("div",Rf,J(e.label),1),h("div",Mf,J(e.hint),1)]),as(t.$slots,"right")]))}}),Pf={class:"modal-hd"},kf={class:"modal-body"},Of={class:"form-row"},If={class:"input"},Nf=["value"],Lf={class:"form-row"},Df={class:"input"},Hf=["placeholder"],Vf={class:"modal-ft"},$f=we({__name:"AddSourceModal",props:{kind:{}},emits:["close"],setup(e,{emit:t}){const n=e,s=t,i=Do(),r=n.kind==="playlist"?"M3U Playlist":"EPG Source";function o(){s("close"),i.push(n.kind==="playlist"?"/playlists":"/epg-sources")}function a(){s("close"),i.push("/import")}return(l,u)=>(A(),k("div",{class:"modal-bg",onClick:u[3]||(u[3]=c=>s("close"))},[h("div",{class:"modal",onClick:u[2]||(u[2]=Eo(()=>{},["stop"]))},[h("div",Pf,[L(me,{name:e.kind==="playlist"?"playlist":"epg",size:18},null,8,["name"]),h("h2",null,"Add "+J(he(r)),1),u[4]||(u[4]=h("span",{class:"spacer"},null,-1)),L($e,{variant:"ghost",size:"sm",icon:"x",onClick:u[0]||(u[0]=c=>s("close"))})]),h("div",kf,[h("div",Of,[u[5]||(u[5]=h("div",{class:"field-lbl"},"Name",-1)),h("div",If,[h("input",{value:`My ${he(r)}`},null,8,Nf)])]),h("div",Lf,[u[6]||(u[6]=h("div",{class:"field-lbl"},"Remote URL",-1)),h("div",Df,[L(me,{name:"link",size:14}),h("input",{placeholder:e.kind==="playlist"?"https://provider.example.com/playlist.m3u":"https://example.com/guide.xml.gz"},null,8,Hf)])]),u[9]||(u[9]=Lt('
Refresh interval
Auth (optional)
',1)),L(Tf,{label:"Auto-match channels to EPG",hint:"Run fuzzy matching right after the first import."},{right:be(()=>[L(Sf,{on:!0})]),_:1}),h("div",{class:"muted",style:{"font-size":"var(--fs-xs)",padding:"4px 0"}},[u[7]||(u[7]=pe(" Tip: you can also drag a file into the ",-1)),h("span",{style:{color:"var(--accent-hi)",cursor:"default"},onClick:a},"Import"),u[8]||(u[8]=pe(" screen. ",-1))])]),h("div",Vf,[L($e,{variant:"ghost",onClick:u[1]||(u[1]=c=>s("close"))},{default:be(()=>[...u[10]||(u[10]=[pe("Cancel",-1)])]),_:1}),L($e,{variant:"primary",icon:"check",onClick:o},{default:be(()=>[...u[11]||(u[11]=[pe("Add & sync",-1)])]),_:1})])])]))}}),Ff=["value","placeholder"],Bf=we({__name:"SearchInput",props:{value:{},placeholder:{},width:{}},emits:["change"],setup(e,{emit:t}){const n=e,s=t;return(i,r)=>(A(),k("div",{class:"input",style:dt({width:(n.width||260)+"px"})},[L(me,{name:"search",size:14}),h("input",{value:e.value,placeholder:e.placeholder||"Search",onInput:r[0]||(r[0]=o=>s("change",o.target.value))},null,40,Ff)],4))}}),Uf={class:"logs-drawer-wrap"},jf={class:"glass logs-drawer"},Gf={class:"logs-hd"},Kf={class:"row",style:{gap:"8px"}},zf={key:0,class:"live-pill",style:{background:"oklch(0.78 0.16 150 / 0.18)",color:"var(--good)","border-color":"oklch(0.78 0.16 150 / 0.4)"}},Wf={class:"muted mono",style:{"font-size":"11px","margin-left":"6px"}},qf={class:"select",style:{"min-width":"110px"}},Yf=["value"],Jf={key:0,class:"empty",style:{padding:"40px"}},Qf={class:"log-ts mono"},Xf={class:"log-src"},Zf={class:"log-msg"},ed=we({__name:"LogsDrawer",emits:["close"],setup(e,{emit:t}){const n=t,s=["sync","epg","stream","match","api","core"],i=[{v:"info",weight:60,color:"var(--text-2)"},{v:"ok",weight:15,color:"var(--good)"},{v:"warn",weight:18,color:"var(--warn)"},{v:"error",weight:4,color:"var(--bad)"},{v:"debug",weight:3,color:"var(--text-3)"}],r=[["sync","info","Refreshing playlist {pl}"],["sync","ok","Playlist {pl} synced in {ms}ms · {n} channels, +{added} −{removed}"],["sync","warn","Playlist {pl} returned 3 channel(s) with 503 — retry in 30s"],["epg","info","Fetching EPG source {epg}"],["epg","ok","Indexed {programs} programs from {epg} in {ms}ms"],["epg","debug","Parsed nodes: {programs}"],["stream","info","Probing upstream {url}"],["stream","ok","Stream {ch} healthy · {bitrate} Mbps, latency {lat}s"],["stream","warn","Stream {ch} dropped {n} frames in last 60s"],["stream","error","Stream {ch} upstream HTTP 503 — pausing relay"],["match","info","Auto-matching {n} channels against EPG IDs"],["match","ok","Mapped {ch} → {tvg}"],["match","warn","Could not auto-match {ch} (no candidate over 0.8 confidence)"],["api","info","GET /v1/playlists/{pl} 200 · {ms}ms"],["api","info","GET /v1/epg/{epg}/programs 200 · {ms}ms"],["api","warn","GET /v1/streams 429 · rate limit"],["core","info","Cache warm: {n} entries, {kb}KB"],["core","debug","GC reclaimed {kb}KB"]];function o(z){const D=r[z%r.length],q=["Default","IPTV-Pro Main","Free UK Bouquet","Archive (legacy)"][z%4],et=["Default","XMLTV UK Guide","iptv-org world EPG"][z%3],Le=Ws[z%Ws.length],Pt=Le.tvg_id||"bbc.one.uk",St=`http://edge-fra-04/live/${Le.id.replace("ch-","ch")}/index.m3u8`,ge=60+z*17%800,ee=(3+z*5%50/10).toFixed(1),X=(1.2+z*7%30/10).toFixed(1),ze=z*11%12+1,tt=z%7,je=(z>>1)%4,Ae=1500+z*53%9e3,Ct=50+z*31%950,At=D[2].replace("{pl}",q).replace("{epg}",et).replace("{ch}",Le.tvg_name).replace("{tvg}",Pt).replace("{url}",St).replace("{ms}",String(ge)).replace("{bitrate}",ee).replace("{lat}",X).replace("{n}",String(ze)).replace("{added}",String(tt)).replace("{removed}",String(je)).replace("{programs}",Ae.toLocaleString()).replace("{kb}",String(Ct));return{source:D[0],level:D[1],text:At}}const a=Array.from({length:36},(z,D)=>{const q=o(D*13+5);return{ts:new Date(Date.now()-(36-D)*(1200+D*137)),id:"seed-"+D,...q}}),l=Oe(a),u=Oe(!1),c=Oe("all"),p=Oe("all"),g=Oe(""),v=Oe(!0),T=Oe(null);let C=0,y=null;function $(){y&&clearInterval(y),y=window.setInterval(()=>{if(u.value)return;C++;const z={...o(Date.now()+C),ts:new Date,id:"live-"+C},D=[...l.value,z];l.value=D.length>400?D.slice(D.length-400):D},700+Math.random()*900)}function E(z){z.key==="Escape"&&n("close")}os(()=>{$(),window.addEventListener("keydown",E)}),ls(()=>{y&&clearInterval(y),window.removeEventListener("keydown",E)}),Yt(l,async()=>{v.value&&T.value&&(await is(),T.value.scrollTop=T.value.scrollHeight)});const N=_e(()=>l.value.filter(z=>(c.value==="all"||c.value===z.level||c.value==="issues"&&(z.level==="warn"||z.level==="error"))&&(p.value==="all"||z.source===p.value)&&(g.value===""||z.text.toLowerCase().includes(g.value.toLowerCase())))),V=_e(()=>({total:l.value.length,warn:l.value.filter(z=>z.level==="warn").length,error:l.value.filter(z=>z.level==="error").length}));function Z(z){const D=z.target,q=D.scrollHeight-D.scrollTop-D.clientHeight<24;q!==v.value&&(v.value=q)}function fe(){v.value=!0,T.value&&(T.value.scrollTop=T.value.scrollHeight)}function re(z){return z.toLocaleTimeString("en-GB",{hour12:!1})+"."+String(z.getMilliseconds()).padStart(3,"0")}function Ue(z){var D;return((D=i.find(q=>q.v===z))==null?void 0:D.color)||"var(--text-2)"}return(z,D)=>(A(),k("div",Uf,[h("div",{class:"glass-bg logs-drawer-backdrop",onClick:D[0]||(D[0]=q=>n("close"))}),h("div",jf,[h("div",Gf,[h("div",Kf,[L(me,{name:"file",size:15}),D[9]||(D[9]=h("span",{style:{"font-weight":"600","font-size":"14px"}},"Realtime logs",-1)),u.value?He("",!0):(A(),k("span",zf,[...D[7]||(D[7]=[h("span",{class:"dot",style:{background:"var(--good)","box-shadow":"0 0 8px var(--good)"}},null,-1),pe("STREAMING ",-1)])])),u.value?(A(),We(cn,{key:1,tone:"warn"},{default:be(()=>[L(me,{name:"pause",size:11}),D[8]||(D[8]=pe("paused",-1))]),_:1})):He("",!0),h("span",Wf,J(V.value.total)+" lines · "+J(V.value.warn)+" warn · "+J(V.value.error)+" err ",1)]),L(Bf,{value:g.value,onChange:D[1]||(D[1]=q=>g.value=q),placeholder:"Filter logs",width:220},null,8,["value"]),h("div",qf,[Al(h("select",{"onUpdate:modelValue":D[2]||(D[2]=q=>p.value=q)},[D[10]||(D[10]=h("option",{value:"all"},"All sources",-1)),(A(),k(j,null,Ft(s,q=>h("option",{key:q,value:q},J(q),9,Yf)),64))],512),[[Xa,p.value]])]),L(Ho,{value:c.value,onChange:D[3]||(D[3]=q=>c.value=q),options:[{value:"all",label:"All"},{value:"info",label:"Info"},{value:"issues",label:"Issues"},{value:"debug",label:"Debug"}]},null,8,["value"]),D[13]||(D[13]=h("span",{class:"spacer"},null,-1)),L($e,{variant:"ghost",size:"sm",icon:u.value?"play":"pause",onClick:D[4]||(D[4]=q=>u.value=!u.value)},{default:be(()=>[pe(J(u.value?"Resume":"Pause"),1)]),_:1},8,["icon"]),L($e,{variant:"ghost",size:"sm",icon:"trash",onClick:D[5]||(D[5]=q=>l.value=[]),title:"Clear"},{default:be(()=>[...D[11]||(D[11]=[pe("Clear",-1)])]),_:1}),L($e,{variant:"ghost",size:"sm",icon:"upload",title:"Download log"},{default:be(()=>[...D[12]||(D[12]=[pe("Export",-1)])]),_:1}),L($e,{variant:"ghost",size:"sm",icon:"x",onClick:D[6]||(D[6]=q=>n("close")),title:"Close (Esc)"})]),h("div",{class:"logs-body",ref_key:"body",ref:T,onScroll:Z},[N.value.length===0?(A(),k("div",Jf,[...D[14]||(D[14]=[h("h3",null,"No log lines match",-1),h("p",null,"Adjust the search or level filter.",-1)])])):(A(!0),k(j,{key:1},Ft(N.value,q=>(A(),k("div",{key:q.id,class:Be(`log-line log-${q.level}`)},[h("span",Qf,J(re(q.ts)),1),h("span",{class:"log-lvl",style:dt({color:Ue(q.level)})},J(q.level.toUpperCase()),5),h("span",Xf,J(q.source),1),h("span",Zf,J(q.text),1)],2))),128))],544),v.value?He("",!0):(A(),k("button",{key:0,class:"logs-scroll-btn",onClick:fe},[L(me,{name:"chevron-d",size:12}),D[15]||(D[15]=pe("Jump to live ",-1))]))])]))}}),qn=Mn({theme:"dark",density:"regular",epgMode:"timeline"});Yt(()=>[qn.theme,qn.density],([e,t])=>{document.documentElement.dataset.theme=e,document.documentElement.dataset.density=t},{immediate:!0});function td(){function e(t,n){var i;const s=typeof t=="object"&&t!==null?t:{[t]:n};Object.assign(qn,s);try{(i=window.parent)==null||i.postMessage({type:"__edit_mode_set_keys",edits:s},"*")}catch{}}return{tweaks:qn,setTweak:e}}function nd(e){return{all:e=e||new Map,on:function(t,n){var s=e.get(t);s?s.push(n):e.set(t,[n])},off:function(t,n){var s=e.get(t);s&&(n?s.splice(s.indexOf(n)>>>0,1):e.set(t,[]))},emit:function(t,n){var s=e.get(t);s&&s.slice().map(function(i){i(n)}),(s=e.get("*"))&&s.slice().map(function(i){i(t,n)})}}}const ks=nd(),sd={class:"app"},id={class:"sidebar"},rd=["onClick"],od={key:0,class:"dot good pulse",style:{width:"6px",height:"6px"}},ld={key:1,class:"count"},ad=["onClick"],ud={class:"sidebar-foot-stack"},cd={class:"logs-btn-ico"},fd={class:"main"},dd={class:"topbar"},pd={key:0,class:"restore-strip",role:"status","aria-live":"polite"},hd={class:"restore-strip-line"},md={class:"restore-strip-label"},gd={class:"restore-strip-bar"},vd={class:"restore-strip-pct mono"},yd={class:"crumb",style:{"margin-left":"6px"}},bd=["title"],_d={class:"theme-toggle-ico"},wd={class:"theme-toggle-ico"},xd=we({__name:"App",setup(e){const{tweaks:t,setTweak:n}=td(),s=Do(),i=pc(),r=Oe(null),o=Oe(null),a=Oe(!1),l=Oe(null);hn("openChannel",C=>{r.value=C});const u=_e(()=>[{id:"dashboard",label:"Dashboard",icon:"dashboard",path:"/dashboard"},{id:"active",label:"Active Streams",icon:"tv",path:"/active",count:cr.filter(C=>C.status!=="bad").length,live:!0},{id:"playlists",label:"Playlists",icon:"playlist",path:"/playlists",count:Nn.length},{id:"epg-sources",label:"EPG Sources",icon:"epg",path:"/epg-sources",count:Ln.length},{id:"mapping",label:"Channel Mapping",icon:"map",path:"/mapping"},{id:"history",label:"History / Metrics",icon:"file",path:"/history"},{id:"import",label:"Import",icon:"import",path:"/import"},{id:"settings",label:"Settings",icon:"settings",path:"/settings"}]);function c(C){const y=i.name;return y?C==="playlists"&&y==="playlist"||C==="epg-sources"&&y==="epg-detail"?!0:C===y:!1}const p=_e(()=>{switch(i.name){case"dashboard":return{title:"Dashboard",crumb:"Overview"};case"active":return{title:"Active Streams",crumb:`${cr.filter(y=>y.status!=="bad").length} live now`};case"playlists":return{title:"Playlists",crumb:`${Nn.length} sources`};case"epg-sources":return{title:"EPG Sources",crumb:`${Ln.length} sources`};case"mapping":return{title:"Channel Mapping",crumb:"M3U ↔ EPG"};case"history":return{title:"History / Metrics",crumb:"Streaming history"};case"import":return{title:"Import",crumb:"M3U / XMLTV"};case"settings":return{title:"Settings",crumb:"Workspace"};case"playlist":{const y=Nn.find($=>$.id===i.params.id)||Nn[0];return{title:y.name,parent:"Playlists",parentPath:"/playlists",crumb:y.name}}case"epg-detail":{const y=Ln.find($=>$.id===i.params.id)||Ln[0];return{title:y.name,parent:"EPG Sources",parentPath:"/epg-sources",crumb:y.name}}default:return{title:"",crumb:""}}}),g=_e(()=>i.name==="epg-detail"||i.name==="active"?{display:"flex",flexDirection:"column"}:null);function v(C){s.push(C),r.value=null}function T(C){const y=C.items||[];if(!y.length)return;const $=5,E=y.length*$;let N=0;l.value={items:y,idx:0,percent:0,label:y[0].text,kind:y[0].kind};const V=window.setInterval(()=>{N++;const Z=Math.min(100,Math.round(N/E*100)),fe=Math.min(y.length-1,Math.floor(N/$)),re=y[fe];l.value={items:y,idx:fe,percent:Z,label:re.text,kind:re.kind},N>=E&&(clearInterval(V),setTimeout(()=>{l.value=null,ks.emit("tvapp:restore-done")},520))},220)}return os(()=>ks.on("tvapp:restore-start",T)),ls(()=>ks.off("tvapp:restore-start",T)),(C,y)=>{const $=Ul("router-view");return A(),k("div",sd,[h("aside",id,[y[15]||(y[15]=h("div",{class:"brand"},[h("span",{class:"brand-dot"}),pe(" TVApp2 ")],-1)),y[16]||(y[16]=h("div",{class:"nav-group-label"},"Workspace",-1)),(A(!0),k(j,null,Ft(u.value.slice(0,6),E=>(A(),k("div",{key:E.id,class:Be(["nav-item",{active:c(E.id)}]),onClick:N=>v(E.path)},[L(me,{name:E.icon},null,8,["name"]),h("span",null,J(E.label),1),E.live?(A(),k("span",od)):He("",!0),E.count!==void 0?(A(),k("span",ld,J(E.count),1)):He("",!0)],10,rd))),128)),y[17]||(y[17]=h("div",{class:"nav-group-label"},"Actions",-1)),(A(!0),k(j,null,Ft(u.value.slice(6),E=>(A(),k("div",{key:E.id,class:Be(["nav-item",{active:c(E.id)}]),onClick:N=>v(E.path)},[L(me,{name:E.icon},null,8,["name"]),h("span",null,J(E.label),1)],10,ad))),128)),h("div",ud,[h("button",{class:"logs-btn",onClick:y[0]||(y[0]=E=>a.value=!0)},[h("span",cd,[L(me,{name:"file",size:14}),y[11]||(y[11]=h("span",{class:"dot good pulse",style:{width:"6px",height:"6px"}},null,-1))]),y[12]||(y[12]=h("span",{style:{flex:"1","text-align":"left"}},"View logs",-1)),y[13]||(y[13]=h("span",{class:"mono",style:{"font-size":"10px",color:"var(--text-3)"}},"live",-1))]),y[14]||(y[14]=Lt('',1))])]),h("main",fd,[h("header",dd,[h("h1",null,J(p.value.title),1),l.value?(A(),k("div",pd,[h("div",hd,[L(me,{name:l.value.kind||"refresh",size:12},null,8,["name"]),y[18]||(y[18]=h("span",{class:"restore-strip-action"},"Restoring",-1)),h("span",md,J(l.value.label),1)]),h("div",gd,[h("div",{class:"restore-strip-fill",style:dt({width:l.value.percent+"%"})},null,4)]),h("span",vd,J(l.value.percent)+"%",1)])):(A(),k(j,{key:1},[h("span",yd,[p.value.parent?(A(),k(j,{key:0},[h("span",{style:{cursor:"default"},onClick:y[1]||(y[1]=E=>v(p.value.parentPath))},J(p.value.parent),1),y[19]||(y[19]=h("span",{style:{color:"var(--text-3)",margin:"0 6px"}},"›",-1))],64)):He("",!0),pe(" "+J(p.value.crumb),1)]),y[20]||(y[20]=h("span",{class:"topbar-spacer"},null,-1))],64)),h("button",{class:"theme-toggle",onClick:y[2]||(y[2]=E=>he(n)("theme",he(t).theme==="dark"?"light":"dark")),title:he(t).theme==="dark"?"Switch to light mode":"Switch to dark mode","aria-label":"Toggle theme"},[h("span",{class:Be(["theme-toggle-thumb",he(t).theme==="dark"?"is-dark":"is-light"])},[L(me,{name:he(t).theme==="dark"?"moon":"sun",size:13},null,8,["name"])],2),h("span",_d,[L(me,{name:"sun",size:13})]),h("span",wd,[L(me,{name:"moon",size:13})])],8,bd),he(i).name==="dashboard"?(A(),We($e,{key:2,variant:"primary",icon:"plus",onClick:y[3]||(y[3]=E=>v("/import"))},{default:be(()=>[...y[21]||(y[21]=[pe("New source",-1)])]),_:1})):He("",!0)]),h("div",{class:"screen",style:dt(g.value||void 0)},[L($,null,{default:be(({Component:E})=>[(A(),We(jl(E),{onAdd:y[4]||(y[4]=N=>o.value=N)},null,32))]),_:1})],4)]),r.value?(A(),We(xf,{key:0,ch:r.value,onClose:y[5]||(y[5]=E=>r.value=null)},null,8,["ch"])):He("",!0),o.value?(A(),We($f,{key:1,kind:o.value,onClose:y[6]||(y[6]=E=>o.value=null)},null,8,["kind"])):He("",!0),a.value?(A(),We(ed,{key:2,onClose:y[7]||(y[7]=E=>a.value=!1)})):He("",!0),L(Sc,{title:"Tweaks"},{default:be(()=>[L(Ms,{label:"Theme"}),L(Ts,{label:"Mode",value:he(t).theme,options:["light","dark"],onChange:y[8]||(y[8]=E=>he(n)("theme",E))},null,8,["value"]),L(Ms,{label:"Layout"}),L(Ts,{label:"Density",value:he(t).density,options:["compact","regular","spacious"],onChange:y[9]||(y[9]=E=>he(n)("density",E))},null,8,["value"]),L(Ms,{label:"EPG view"}),L(Ts,{label:"Style",value:he(t).epgMode,options:["timeline","list"],onChange:y[10]||(y[10]=E=>he(n)("epgMode",E))},null,8,["value"])]),_:1})])}}}),Ed="modulepreload",Sd=function(e){return"/"+e},fr={},ot=function(t,n,s){let i=Promise.resolve();if(n&&n.length>0){let o=function(u){return Promise.all(u.map(c=>Promise.resolve(c).then(p=>({status:"fulfilled",value:p}),p=>({status:"rejected",reason:p}))))};document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),l=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));i=o(n.map(u=>{if(u=Sd(u),u in fr)return;fr[u]=!0;const c=u.endsWith(".css"),p=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${u}"]${p}`))return;const g=document.createElement("link");if(g.rel=c?"stylesheet":Ed,c||(g.as="script"),g.crossOrigin="",g.href=u,l&&g.setAttribute("nonce",l),document.head.appendChild(g),c)return new Promise((v,T)=>{g.addEventListener("load",v),g.addEventListener("error",()=>T(new Error(`Unable to preload CSS for ${u}`)))})}))}function r(o){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=o,window.dispatchEvent(a),!a.defaultPrevented)throw o}return i.then(o=>{for(const a of o||[])a.status==="rejected"&&r(a.reason);return t().catch(r)})},Cd=[{path:"/",redirect:"/active"},{path:"/dashboard",name:"dashboard",component:()=>ot(()=>import("./DashboardScreen-C8mAn1wl.js"),[])},{path:"/active",name:"active",component:()=>ot(()=>import("./ActiveStreamsScreen-Df8Wdk9n.js"),[])},{path:"/playlists",name:"playlists",component:()=>ot(()=>import("./PlaylistsScreen-0ooKY6SX.js"),__vite__mapDeps([0,1]))},{path:"/playlists/:id",name:"playlist",component:()=>ot(()=>import("./PlaylistDetailScreen-F92VSAQ7.js"),__vite__mapDeps([2,1,3])),props:!0},{path:"/epg-sources",name:"epg-sources",component:()=>ot(()=>import("./EPGSourcesScreen-Bh2qXoOm.js"),[])},{path:"/epg-sources/:id",name:"epg-detail",component:()=>ot(()=>import("./EPGDetailScreen-CX4y1Ve9.js"),__vite__mapDeps([4,3])),props:!0},{path:"/mapping",name:"mapping",component:()=>ot(()=>import("./MappingScreen-BdiMBcth.js"),__vite__mapDeps([5,3]))},{path:"/history",name:"history",component:()=>ot(()=>import("./HistoryMetricsScreen-DAcDrLQC.js"),[])},{path:"/import",name:"import",component:()=>ot(()=>import("./ImportScreen-D7vLRk6-.js"),[])},{path:"/settings",name:"settings",component:()=>ot(()=>import("./SettingsScreen-Daj_a2gr.js"),__vite__mapDeps([6,1]))}],Ad=dc({history:Gu(),routes:Cd});su(xd).use(Ad).mount("#app");export{cr as A,ls as B,Ws as C,os as D,Td as E,j as F,Lc as G,A as H,Mn as I,Oe as J,Ft as K,as as L,Od as M,J as N,he as O,Nn as P,Do as Q,td as R,kd as S,Xa as T,Md as U,Yt as V,Rd as W,be as X,Al as Y,Eo as Z,cn as _,Pd as a,Id as b,$c as c,Ln as d,$e as e,me as f,kc as g,Ho as h,Oc as i,Bf as j,Sf as k,Tf as l,ks as m,_e as n,h as o,We as p,He as q,k as r,Lt as s,pe as t,L as u,we as v,Ye as w,Te as x,Be as y,dt as z}; diff --git a/dist/assets/useSettings-CPUgOpin.js b/dist/assets/useSettings-CPUgOpin.js new file mode 100644 index 00000000..39d96b90 --- /dev/null +++ b/dist/assets/useSettings-CPUgOpin.js @@ -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}; diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 00000000..ded17823 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,19 @@ + + + + + + TVApp2 — M3U & EPG Manager + + + + + + + + +
+ + diff --git a/dist/types-node/vite.config.d.ts b/dist/types-node/vite.config.d.ts new file mode 100644 index 00000000..340562af --- /dev/null +++ b/dist/types-node/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/dist/types-node/vite.config.js b/dist/types-node/vite.config.js new file mode 100644 index 00000000..59bf3c52 --- /dev/null +++ b/dist/types-node/vite.config.js @@ -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, + }, + }, + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..91c3edd8 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile new file mode 100644 index 00000000..af1bd491 --- /dev/null +++ b/docker/app.Dockerfile @@ -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(,'..','public') => /app/public) +# /app/seed-data/ source bundles (resolve(,'..','..','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"] diff --git a/index.html b/index.html new file mode 100644 index 00000000..08156764 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + TVApp2 — M3U & EPG Manager + + + + + + +
+ + + diff --git a/m3u-data/export - M3U - Grid.xlsx b/m3u-data/export - M3U - Grid.xlsx new file mode 100644 index 00000000..1c0fe676 Binary files /dev/null and b/m3u-data/export - M3U - Grid.xlsx differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4ddc2214 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1575 @@ +{ + "name": "tvapp2", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tvapp2", + "version": "0.1.0", + "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" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "vue": "3.5.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..5eed3d71 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server/config.local.json b/server/config.local.json new file mode 100644 index 00000000..4f151f75 --- /dev/null +++ b/server/config.local.json @@ -0,0 +1,5 @@ +{ + "mongoUri": "mongodb://tvapp:tvapp@127.0.0.1:27017/tvapp2?authSource=admin", + "port": 3000, + "logLevel": "debug" +} diff --git a/server/dist/config.js b/server/dist/config.js new file mode 100644 index 00000000..174dc582 --- /dev/null +++ b/server/dist/config.js @@ -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 \ No newline at end of file diff --git a/server/dist/config.js.map b/server/dist/config.js.map new file mode 100644 index 00000000..62981124 --- /dev/null +++ b/server/dist/config.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/db.js b/server/dist/db.js new file mode 100644 index 00000000..22cf85c8 --- /dev/null +++ b/server/dist/db.js @@ -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 \ No newline at end of file diff --git a/server/dist/db.js.map b/server/dist/db.js.map new file mode 100644 index 00000000..8d14693c --- /dev/null +++ b/server/dist/db.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/index.js b/server/dist/index.js new file mode 100644 index 00000000..b7113527 --- /dev/null +++ b/server/dist/index.js @@ -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 \ No newline at end of file diff --git a/server/dist/index.js.map b/server/dist/index.js.map new file mode 100644 index 00000000..e73ba841 --- /dev/null +++ b/server/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;QAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,8FAA8F;IAC9F,iGAAiG;IACjG,mEAAmE;IACnE,IAAI,CAAC;QACH,MAAM,eAAe,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACrC,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAC3C,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC;IAC9C,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,mBAAmB,CAAC,CAAC;IACpD,GAAG,CAAC,GAAG,CAAC,uBAAuB,EAAE,qBAAqB,CAAC,CAAC;IACxD,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;IAC7C,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,CAAC;IACtD,8FAA8F;IAC9F,uCAAuC;IACvC,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAEvB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAChD,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACnC,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YACtC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,2BAA2B,SAAS,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,CAAC,GAAU,EAAE,IAAqB,EAAE,GAAqB,EAAE,KAA2B,EAAE,EAAE;QAChG,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QAC1C,OAAO,CAAC,IAAI,CAAC,uBAAuB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,OAAO,CAAC,IAAI,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,UAAU,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/server/dist/models/ActiveStream.js b/server/dist/models/ActiveStream.js new file mode 100644 index 00000000..4da1647d --- /dev/null +++ b/server/dist/models/ActiveStream.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/ActiveStream.js.map b/server/dist/models/ActiveStream.js.map new file mode 100644 index 00000000..87b354c2 --- /dev/null +++ b/server/dist/models/ActiveStream.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/Activity.js b/server/dist/models/Activity.js new file mode 100644 index 00000000..6d49c106 --- /dev/null +++ b/server/dist/models/Activity.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/Activity.js.map b/server/dist/models/Activity.js.map new file mode 100644 index 00000000..398b650a --- /dev/null +++ b/server/dist/models/Activity.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/Channel.js b/server/dist/models/Channel.js new file mode 100644 index 00000000..7abf38e1 --- /dev/null +++ b/server/dist/models/Channel.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/Channel.js.map b/server/dist/models/Channel.js.map new file mode 100644 index 00000000..15db582b --- /dev/null +++ b/server/dist/models/Channel.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/CustomPlaylist.js b/server/dist/models/CustomPlaylist.js new file mode 100644 index 00000000..8a0eee73 --- /dev/null +++ b/server/dist/models/CustomPlaylist.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/CustomPlaylist.js.map b/server/dist/models/CustomPlaylist.js.map new file mode 100644 index 00000000..f5a3fd22 --- /dev/null +++ b/server/dist/models/CustomPlaylist.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/EpgSource.js b/server/dist/models/EpgSource.js new file mode 100644 index 00000000..b7e27b0d --- /dev/null +++ b/server/dist/models/EpgSource.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/EpgSource.js.map b/server/dist/models/EpgSource.js.map new file mode 100644 index 00000000..56ca7dae --- /dev/null +++ b/server/dist/models/EpgSource.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/Playlist.js b/server/dist/models/Playlist.js new file mode 100644 index 00000000..4d224015 --- /dev/null +++ b/server/dist/models/Playlist.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/Playlist.js.map b/server/dist/models/Playlist.js.map new file mode 100644 index 00000000..42918d97 --- /dev/null +++ b/server/dist/models/Playlist.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/PlaylistChannel.js b/server/dist/models/PlaylistChannel.js new file mode 100644 index 00000000..3cccb2a3 --- /dev/null +++ b/server/dist/models/PlaylistChannel.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/PlaylistChannel.js.map b/server/dist/models/PlaylistChannel.js.map new file mode 100644 index 00000000..14eaab52 --- /dev/null +++ b/server/dist/models/PlaylistChannel.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/Program.js b/server/dist/models/Program.js new file mode 100644 index 00000000..a7c86a3b --- /dev/null +++ b/server/dist/models/Program.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/Program.js.map b/server/dist/models/Program.js.map new file mode 100644 index 00000000..ed431343 --- /dev/null +++ b/server/dist/models/Program.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/SourceChannel.js b/server/dist/models/SourceChannel.js new file mode 100644 index 00000000..a3d66271 --- /dev/null +++ b/server/dist/models/SourceChannel.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/SourceChannel.js.map b/server/dist/models/SourceChannel.js.map new file mode 100644 index 00000000..cd4c444c --- /dev/null +++ b/server/dist/models/SourceChannel.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/models/StreamSession.js b/server/dist/models/StreamSession.js new file mode 100644 index 00000000..035c5e95 --- /dev/null +++ b/server/dist/models/StreamSession.js @@ -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 \ No newline at end of file diff --git a/server/dist/models/StreamSession.js.map b/server/dist/models/StreamSession.js.map new file mode 100644 index 00000000..531d9084 --- /dev/null +++ b/server/dist/models/StreamSession.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/activeStreams.js b/server/dist/routes/activeStreams.js new file mode 100644 index 00000000..f4805881 --- /dev/null +++ b/server/dist/routes/activeStreams.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/activeStreams.js.map b/server/dist/routes/activeStreams.js.map new file mode 100644 index 00000000..afbe4c41 --- /dev/null +++ b/server/dist/routes/activeStreams.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/activity.js b/server/dist/routes/activity.js new file mode 100644 index 00000000..7efab394 --- /dev/null +++ b/server/dist/routes/activity.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/activity.js.map b/server/dist/routes/activity.js.map new file mode 100644 index 00000000..bc02bcca --- /dev/null +++ b/server/dist/routes/activity.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/channels.js b/server/dist/routes/channels.js new file mode 100644 index 00000000..8dec7038 --- /dev/null +++ b/server/dist/routes/channels.js @@ -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= → 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 \ No newline at end of file diff --git a/server/dist/routes/channels.js.map b/server/dist/routes/channels.js.map new file mode 100644 index 00000000..54b8acbd --- /dev/null +++ b/server/dist/routes/channels.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/customPlaylists.js b/server/dist/routes/customPlaylists.js new file mode 100644 index 00000000..089dcbae --- /dev/null +++ b/server/dist/routes/customPlaylists.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/customPlaylists.js.map b/server/dist/routes/customPlaylists.js.map new file mode 100644 index 00000000..b0c1c9c9 --- /dev/null +++ b/server/dist/routes/customPlaylists.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/epgSources.js b/server/dist/routes/epgSources.js new file mode 100644 index 00000000..cb9a952d --- /dev/null +++ b/server/dist/routes/epgSources.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/epgSources.js.map b/server/dist/routes/epgSources.js.map new file mode 100644 index 00000000..b936d2d8 --- /dev/null +++ b/server/dist/routes/epgSources.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/health.js b/server/dist/routes/health.js new file mode 100644 index 00000000..bd8dc5e8 --- /dev/null +++ b/server/dist/routes/health.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/health.js.map b/server/dist/routes/health.js.map new file mode 100644 index 00000000..fac6e75b --- /dev/null +++ b/server/dist/routes/health.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/playlists.js b/server/dist/routes/playlists.js new file mode 100644 index 00000000..71585964 --- /dev/null +++ b/server/dist/routes/playlists.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/playlists.js.map b/server/dist/routes/playlists.js.map new file mode 100644 index 00000000..44995a87 --- /dev/null +++ b/server/dist/routes/playlists.js.map @@ -0,0 +1 @@ +{"version":3,"file":"playlists.js","sourceRoot":"","sources":["../../src/routes/playlists.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/server/dist/routes/programs.js b/server/dist/routes/programs.js new file mode 100644 index 00000000..34e579ae --- /dev/null +++ b/server/dist/routes/programs.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/programs.js.map b/server/dist/routes/programs.js.map new file mode 100644 index 00000000..808a478a --- /dev/null +++ b/server/dist/routes/programs.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/routes/sources.js b/server/dist/routes/sources.js new file mode 100644 index 00000000..10782950 --- /dev/null +++ b/server/dist/routes/sources.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/sources.js.map b/server/dist/routes/sources.js.map new file mode 100644 index 00000000..0bd7f3fb --- /dev/null +++ b/server/dist/routes/sources.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/routes/sources.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,4FAA4F;AAC5F,EAAE;AACF,+FAA+F;AAC/F,2FAA2F;AAC3F,8DAA8D;AAC9D,4FAA4F;AAC5F,0EAA0E;AAC1E,iGAAiG;AACjG,4FAA4F;AAC5F,EAAE;AACF,mGAAmG;AAEnG,OAAO,EAAE,MAAM,EAAuB,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAgB,MAAM,4BAA4B,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,EAAE,CAAC;AAEtC,iGAAiG;AACjG,MAAM,WAAW,GAAG,IAAI,GAAG,EAAmB,CAAC;AAC/C,MAAM,aAAa,GAAG,IAAI,GAAG,EAA0B,CAAC;AACxD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;IAC9B,MAAM,CAAC,GAAG,aAAa,EAAE,CAAC;IAC1B,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/B,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,kBAAkB,CAAC,OAAO,EAAE,CAAC,CAAmB,CAAC,CAAC;AAClF,CAAC;AAED,+EAA+E;AAC/E,aAAa,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC9C,GAAG,CAAC,IAAI,CACN,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClB,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,wBAAwB,CAAC,CAAC,EAAE,EAAE,EAAE,gCAAgC;QAC3E,WAAW,EAAE,WAAW,CAAC,CAAC,EAAE,GAAG;QAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI;KAC3D,CAAC,CAAC,CACJ,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,yFAAyF;AACzF,aAAa,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACpE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACzF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,iFAAiF;AACjF,aAAa,CAAC,GAAG,CAAC,0BAA0B,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACzD,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACnF,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC,CAAC,CAAC;AAEH,iFAAiF;AACjF,aAAa,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACnE,IAAI,CAAC;QACH,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1G,GAAG,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,iFAAiF;AACjF,aAAa,CAAC,IAAI,CAAC,wBAAwB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACpE,IAAI,CAAC;QACH,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1G,GAAG,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,iFAAiF;AACjF,aAAa,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAClD,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,mBAAmB,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/server/dist/routes/streamSessions.js b/server/dist/routes/streamSessions.js new file mode 100644 index 00000000..9e219ac7 --- /dev/null +++ b/server/dist/routes/streamSessions.js @@ -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 \ No newline at end of file diff --git a/server/dist/routes/streamSessions.js.map b/server/dist/routes/streamSessions.js.map new file mode 100644 index 00000000..c1b9b609 --- /dev/null +++ b/server/dist/routes/streamSessions.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/seed.js b/server/dist/seed.js new file mode 100644 index 00000000..17011382 --- /dev/null +++ b/server/dist/seed.js @@ -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: 'IPTV-Pro Main synced — 142 channels, no changes' }, + { when: '12m', icon: 'epg', html: 'XMLTV UK Guide imported — 8,420 programs across 124 channels' }, + { when: '1h', icon: 'map', html: 'Manual mapping: HGTVhgtv.uk' }, + { when: '1h', icon: 'warn', html: 'Free UK Bouquet reports 3 channels offline (HTTP 503)' }, + { when: '3h', icon: 'edit', html: 'Renamed DiscoveryDiscovery Channel' }, + { when: 'Yest.', icon: 'add', html: 'Playlist IPTV-Pro Main 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 \ No newline at end of file diff --git a/server/dist/seed.js.map b/server/dist/seed.js.map new file mode 100644 index 00000000..b2270a58 --- /dev/null +++ b/server/dist/seed.js.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/server/dist/sources/adapters/dulo.js b/server/dist/sources/adapters/dulo.js new file mode 100644 index 00000000..aa2ab22b --- /dev/null +++ b/server/dist/sources/adapters/dulo.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/adapters/dulo.js.map b/server/dist/sources/adapters/dulo.js.map new file mode 100644 index 00000000..f49afb49 --- /dev/null +++ b/server/dist/sources/adapters/dulo.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dulo.js","sourceRoot":"","sources":["../../../src/sources/adapters/dulo.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/server/dist/sources/core/buildSource.js b/server/dist/sources/core/buildSource.js new file mode 100644 index 00000000..df155e8c --- /dev/null +++ b/server/dist/sources/core/buildSource.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/core/buildSource.js.map b/server/dist/sources/core/buildSource.js.map new file mode 100644 index 00000000..603e7407 --- /dev/null +++ b/server/dist/sources/core/buildSource.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/core/logger.js b/server/dist/sources/core/logger.js new file mode 100644 index 00000000..c6c95b34 --- /dev/null +++ b/server/dist/sources/core/logger.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/core/logger.js.map b/server/dist/sources/core/logger.js.map new file mode 100644 index 00000000..3a0666c3 --- /dev/null +++ b/server/dist/sources/core/logger.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/core/metrics.js b/server/dist/sources/core/metrics.js new file mode 100644 index 00000000..f16b7c79 --- /dev/null +++ b/server/dist/sources/core/metrics.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/core/metrics.js.map b/server/dist/sources/core/metrics.js.map new file mode 100644 index 00000000..e05437b1 --- /dev/null +++ b/server/dist/sources/core/metrics.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/core/playlist.js b/server/dist/sources/core/playlist.js new file mode 100644 index 00000000..5aa59c96 --- /dev/null +++ b/server/dist/sources/core/playlist.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/core/playlist.js.map b/server/dist/sources/core/playlist.js.map new file mode 100644 index 00000000..e74b4748 --- /dev/null +++ b/server/dist/sources/core/playlist.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/core/proxyHandler.js b/server/dist/sources/core/proxyHandler.js new file mode 100644 index 00000000..02b4860d --- /dev/null +++ b/server/dist/sources/core/proxyHandler.js @@ -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// +// · entry URL (dlhd watch.php / stream-N.php) → adapter.resolveStream() → fresh master, then proxy +// · master/variant .m3u8 → rewrite child URIs back through /api/v1//… +// · 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 \ No newline at end of file diff --git a/server/dist/sources/core/proxyHandler.js.map b/server/dist/sources/core/proxyHandler.js.map new file mode 100644 index 00000000..0be1c8ce --- /dev/null +++ b/server/dist/sources/core/proxyHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"proxyHandler.js","sourceRoot":"","sources":["../../../src/sources/core/proxyHandler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/server/dist/sources/paths.js b/server/dist/sources/paths.js new file mode 100644 index 00000000..22ee05cb --- /dev/null +++ b/server/dist/sources/paths.js @@ -0,0 +1,16 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +// This module lives at /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 .source.json baselines + .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 \ No newline at end of file diff --git a/server/dist/sources/paths.js.map b/server/dist/sources/paths.js.map new file mode 100644 index 00000000..763e4f87 --- /dev/null +++ b/server/dist/sources/paths.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/registry.js b/server/dist/sources/registry.js new file mode 100644 index 00000000..19a9c5ef --- /dev/null +++ b/server/dist/sources/registry.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/registry.js.map b/server/dist/sources/registry.js.map new file mode 100644 index 00000000..d553929b --- /dev/null +++ b/server/dist/sources/registry.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/seed.js b/server/dist/sources/seed.js new file mode 100644 index 00000000..8d0f43ed --- /dev/null +++ b/server/dist/sources/seed.js @@ -0,0 +1,137 @@ +// Seed / init / sync / reset for the established (Default) source playlists. +// +// "Both: bundle + live sync" — the committed .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 \ No newline at end of file diff --git a/server/dist/sources/seed.js.map b/server/dist/sources/seed.js.map new file mode 100644 index 00000000..01937ee8 --- /dev/null +++ b/server/dist/sources/seed.js.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../src/sources/seed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/server/dist/sources/translate.js b/server/dist/sources/translate.js new file mode 100644 index 00000000..a323ac2f --- /dev/null +++ b/server/dist/sources/translate.js @@ -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 \ No newline at end of file diff --git a/server/dist/sources/translate.js.map b/server/dist/sources/translate.js.map new file mode 100644 index 00000000..e253537b --- /dev/null +++ b/server/dist/sources/translate.js.map @@ -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"} \ No newline at end of file diff --git a/server/dist/sources/types.js b/server/dist/sources/types.js new file mode 100644 index 00000000..31929c07 --- /dev/null +++ b/server/dist/sources/types.js @@ -0,0 +1,6 @@ +// The source-adapter contract, ported from d-combine (sources//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 \ No newline at end of file diff --git a/server/dist/sources/types.js.map b/server/dist/sources/types.js.map new file mode 100644 index 00000000..8aa17192 --- /dev/null +++ b/server/dist/sources/types.js.map @@ -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"} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..3de6a1f2 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1705 @@ +{ + "name": "tvapp2-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tvapp2-server", + "version": "0.1.0", + "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" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.0.tgz", + "integrity": "sha512-EEZwOibDPZ5uZN3bFapfnRskEbdljAf6sP9ln6u+P4e5IfkOAh6Tqw2g8/Tag++KHOAJ095WXT/c0uqRq4Vckg==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..c5119af4 --- /dev/null +++ b/server/package.json @@ -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" + } +} diff --git a/server/seed-data/sources/dulo.snapshot.json b/server/seed-data/sources/dulo.snapshot.json new file mode 100644 index 00000000..0776d264 --- /dev/null +++ b/server/seed-data/sources/dulo.snapshot.json @@ -0,0 +1,1324 @@ +{ + "channels": [ + { + "id": "0b56be49-557e-4315-9258-e9a0728ab608", + "name": "A&E East HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/2afff205-c2ef-4621-a716-62e65d313b5c.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/AandENetwork.us.png", + "created_at": "2026-03-21T02:29:23.917732+00:00", + "updated_at": "2026-05-29T05:38:04.349314+00:00" + }, + { + "id": "5e5ba7f3-6967-4504-9f26-b9f17ec42ead", + "name": "ABC HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/cf396710-6c4d-4c85-9021-df9c24fd6c0b.m3u8", + "epg_source_url": "https://epg.pw/last/467679.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ABC%20%28copy%29.png", + "created_at": "2026-03-21T02:30:00.439868+00:00", + "updated_at": "2026-05-29T05:39:06.728862+00:00" + }, + { + "id": "e2ce7dba-a5e5-4b5c-a935-341f4a08ddeb", + "name": "ACC Network HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/a2a7afae-9bc8-410f-8383-eb1e4117d2f9.m3u8", + "epg_source_url": "https://epg.pw/last/464879.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ACCNetwork.us.png", + "created_at": "2026-03-21T02:30:40.651545+00:00", + "updated_at": "2026-05-31T17:47:37.753669+00:00" + }, + { + "id": "afa5347e-e2ab-403c-8670-61f8c4fd47f1", + "name": "Al Jazeera HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/bd712599-b656-49a7-9613-a60e8615bdf4.m3u8", + "epg_source_url": "https://epg.pw/last/470415.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/AlJazeera.us.png", + "created_at": "2026-03-21T02:31:24.750662+00:00", + "updated_at": "2026-05-29T05:41:06.853417+00:00" + }, + { + "id": "e06e04f5-6e51-42f8-a3c0-7f7bcadb50be", + "name": "AMC HD | USA", + "category": "movies", + "source_url": "https://images.dulo.tv/memfs/a78581f9-fb9a-428c-bfaf-8a6fc0978032.m3u8", + "epg_source_url": "https://epg.pw/last/465032.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/AMC.us.png", + "created_at": "2026-03-21T02:32:01.414801+00:00", + "updated_at": "2026-05-29T06:24:19.490684+00:00" + }, + { + "id": "cd124020-4cc8-460f-b730-e0393015d164", + "name": "Animal Planet HD | USA", + "category": "documentary", + "source_url": "https://hey.dulo.tv/memfs/990a73ff-81d2-4222-92d0-6d1b4330245c.m3u8", + "epg_source_url": "https://epg.pw/last/465310.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/AnimalPlanet.us.png", + "created_at": "2026-03-21T02:32:41.748814+00:00", + "updated_at": "2026-05-31T04:05:36.889853+00:00" + }, + { + "id": "bd829da5-d754-4e02-bdf5-65a14e666c43", + "name": "AT&T SportsNet Pittsburgh HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/f87bbb2d-ae83-4797-9400-e0efc48e975c.m3u8", + "epg_source_url": "https://epg.pw/last/465124.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ATTSportsNetPittsburgh.us.png", + "created_at": "2026-03-21T02:33:30.494861+00:00", + "updated_at": "2026-05-08T20:00:49.000074+00:00" + }, + { + "id": "35427165-289d-43b6-82bc-833c2e6e1341", + "name": "beIN Sports HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/e0d84ef1-56aa-4e76-b04e-86bf01a69759.m3u8", + "epg_source_url": "https://epg.pw/last/464754.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/beINSports.us.png", + "created_at": "2026-03-21T02:34:09.504415+00:00", + "updated_at": "2026-05-08T20:02:21.392525+00:00" + }, + { + "id": "a38ac31c-23fd-42f1-aa43-a5a30a2d7554", + "name": "BET HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/b8299a0d-07ec-40cb-b997-201434cbfefa.m3u8", + "epg_source_url": "https://epg.pw/last/464968.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/BET.us.png", + "created_at": "2026-03-21T02:35:28.778134+00:00", + "updated_at": "2026-05-29T06:31:04.335649+00:00" + }, + { + "id": "15be82f9-7d1c-4035-8132-afcefe933063", + "name": "Big Ten Network HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/b612ee1f-468a-452c-b0fd-48c3ba99ff8a.m3u8", + "epg_source_url": "https://epg.pw/last/465073.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/BigTen.us.png", + "created_at": "2026-03-21T02:36:15.8308+00:00", + "updated_at": "2026-05-29T06:33:06.554696+00:00" + }, + { + "id": "e18e2a96-41e4-4638-8402-f46ada39272d", + "name": "Boomerang HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/b48d96c2-dd3c-4a6c-a5ae-41bf2220a61e.m3u8", + "epg_source_url": "https://epg.pw/last/464755.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Boomerang.us.png", + "created_at": "2026-03-21T02:36:52.17059+00:00", + "updated_at": "2026-05-31T04:03:25.431848+00:00" + }, + { + "id": "9149b685-782f-4560-941a-7624d0857552", + "name": "Bravo East HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/ffe47a87-9e1f-4539-9879-e2fae54d3930.m3u8", + "epg_source_url": "https://epg.pw/last/464753.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Bravo.us.png", + "created_at": "2026-03-21T02:37:32.707957+00:00", + "updated_at": "2026-05-29T06:35:58.875544+00:00" + }, + { + "id": "e4e962a7-7d7c-4361-a271-fc6c52b19466", + "name": "C-SPAN HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/c2be2b4a-5f5f-43d9-acde-0110dec73251.m3u8", + "epg_source_url": "https://epg.pw/last/464835.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CSPAN.us.png", + "created_at": "2026-03-21T02:38:22.183331+00:00", + "updated_at": "2026-05-29T06:36:45.834137+00:00" + }, + { + "id": "2e438a75-284a-4fe9-b64b-9fe3faf0765a", + "name": "Cartoon Network East HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/0ca12fc0-73c4-4c1c-a259-82cd7d90f580.m3u8", + "epg_source_url": "https://epg.pw/last/465267.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CartoonNetwork.us.png", + "created_at": "2026-03-21T02:39:21.717696+00:00", + "updated_at": "2026-05-31T02:31:08.839163+00:00" + }, + { + "id": "5b2ceda1-36f9-4f47-aea9-fa8d04b3aa46", + "name": "CMT HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/3f2389ea-aba0-41e0-95a2-57e020b34858.m3u8", + "epg_source_url": "https://epg.pw/last/465107.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CMT.us.png", + "created_at": "2026-03-21T02:39:48.207806+00:00", + "updated_at": "2026-05-29T06:39:47.470837+00:00" + }, + { + "id": "a55f3e69-7941-4f12-9300-83a61f8f7339", + "name": "CNBC HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/f9d027af-a9d2-4c59-865e-7101f7602966.m3u8", + "epg_source_url": "https://epg.pw/last/464791.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CNBC.us.png", + "created_at": "2026-03-21T02:40:33.795357+00:00", + "updated_at": "2026-05-29T06:40:36.797817+00:00" + }, + { + "id": "d435a43b-8fee-4a39-86c6-dc24f494bdcc", + "name": "CNN HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/bd76a4bc-4dec-4fc7-b200-e0b9f0302dbf.m3u8", + "epg_source_url": "https://epg.pw/last/464857.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CNN.us.png", + "created_at": "2026-03-21T02:41:05.259824+00:00", + "updated_at": "2026-05-29T06:41:29.9035+00:00" + }, + { + "id": "3b00d176-d30d-4818-aeca-04f8bb3a8006", + "name": "Comedy Central HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/e3d94c5a-a969-491a-9527-092b9ebb3424.m3u8", + "epg_source_url": "https://epg.pw/last/464922.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ComedyCentral.us.png", + "created_at": "2026-03-21T02:42:46.336185+00:00", + "updated_at": "2026-05-29T06:43:11.626359+00:00" + }, + { + "id": "4e95c735-dda1-4717-87c1-7ffff63fddff", + "name": "Discovery Channel HD | USA", + "category": "documentary", + "source_url": "https://hey.dulo.tv/memfs/cc4fe5ad-82d3-46cf-b3ec-6214772ce8c9.m3u8", + "epg_source_url": "https://epg.pw/last/465364.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/DiscoveryChannel.us.png", + "created_at": "2026-03-21T02:43:28.368526+00:00", + "updated_at": "2026-05-31T04:06:49.379247+00:00" + }, + { + "id": "463e7954-6e71-4d25-b266-50fccc17a5bd", + "name": "Disney Channel HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/5ae3e3d6-e8de-4e6b-a1e4-44d08c52c1b9.m3u8", + "epg_source_url": "https://epg.pw/last/465349.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/DisneyChannel.us.png", + "created_at": "2026-03-21T02:44:11.811953+00:00", + "updated_at": "2026-05-31T03:57:02.924322+00:00" + }, + { + "id": "3664b2cb-31dc-4274-944b-73136a47f26e", + "name": "Disney Jr HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/edce9dc6-ea78-484d-a6ae-8fc695680c5d.m3u8", + "epg_source_url": "https://epg.pw/last/465320.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/DisneyJunior.us.png", + "created_at": "2026-03-21T02:44:44.857791+00:00", + "updated_at": "2026-05-31T04:41:37.631607+00:00" + }, + { + "id": "b6fbc14a-bcdf-4c4d-88e1-78a94aec05f2", + "name": "E! HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/d72e883f-c569-4c7f-9e6e-7466fcb77df6.m3u8", + "epg_source_url": "https://epg.pw/last/464832.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/EEntertainmentTelevision.us.png", + "created_at": "2026-03-21T02:45:58.725723+00:00", + "updated_at": "2026-05-29T06:47:21.880639+00:00" + }, + { + "id": "e84387d8-0d08-4766-93e0-8d3c39fb721c", + "name": "epiX HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/1e79ae69-e06d-4ace-ae7b-c2d8769c2d46.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Epix.us.png", + "created_at": "2026-03-21T02:46:34.951334+00:00", + "updated_at": "2026-05-31T03:33:14.713361+00:00" + }, + { + "id": "18037a7e-3daa-4ca5-8eb7-40ad0ba09776", + "name": "ESPN 2 HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/762c0cd1-e3d5-4cc6-8b5c-fe058f74634c.m3u8", + "epg_source_url": "https://epg.pw/last/465373.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ESPN2.us.png", + "created_at": "2026-03-21T02:47:11.201701+00:00", + "updated_at": "2026-05-29T06:49:16.830163+00:00" + }, + { + "id": "ef9447e6-bee7-4de3-80bc-9eb215a925df", + "name": "ESPN HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/22d6a3ff-655d-4a28-be98-b9705d80919b.m3u8", + "epg_source_url": "https://epg.pw/last/465198.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ESPN.us.png", + "created_at": "2026-03-21T02:48:55.100839+00:00", + "updated_at": "2026-05-29T06:51:26.627098+00:00" + }, + { + "id": "d80a1087-c4d1-4c0c-9f7d-1c639784ed7e", + "name": "ESPN News HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/a6f4cb11-c02f-4b0b-8fab-ba5147800479.m3u8", + "epg_source_url": "https://epg.pw/last/465410.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ESPNEWS.us.png", + "created_at": "2026-03-21T02:49:37.250592+00:00", + "updated_at": "2026-05-29T06:50:14.086123+00:00" + }, + { + "id": "1263c734-c158-4936-969b-f46e5d21bef4", + "name": "ESPN U HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/69bb2fd6-b379-4612-971c-c613e0868ef2.m3u8", + "epg_source_url": "https://epg.pw/last/465108.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ESPNU.us.png", + "created_at": "2026-03-21T02:50:12.026918+00:00", + "updated_at": "2026-05-29T06:52:30.959232+00:00" + }, + { + "id": "e51f0e58-0f0b-476e-b2a5-84033adcc2c2", + "name": "Food Network HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/d5952b5e-8fd3-42f7-a6c9-468ea8cbec1d.m3u8", + "epg_source_url": "https://epg.pw/last/464984.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoodNetwork.us.png", + "created_at": "2026-03-21T02:51:00.312647+00:00", + "updated_at": "2026-05-29T06:53:48.792833+00:00" + }, + { + "id": "9e26f026-1202-4501-bb0a-faa9edd6e6a8", + "name": "Fox 5 NY HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/f022aa37-6621-4c5a-b321-4a204c75743a.m3u8", + "epg_source_url": "https://epg.pw/last/468913.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoxEast_WNYW.us.png", + "created_at": "2026-03-21T02:51:42.636025+00:00", + "updated_at": "2026-05-29T06:55:12.363287+00:00" + }, + { + "id": "b97baa93-8be4-4dd7-b7bf-29f24fe401f2", + "name": "Fox Business HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/9509f30b-0b7a-4761-b18d-cb47644a414b.m3u8", + "epg_source_url": "https://epg.pw/last/464766.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoxBusiness.us.png", + "created_at": "2026-03-21T02:53:01.618654+00:00", + "updated_at": "2026-05-29T06:56:29.271963+00:00" + }, + { + "id": "509cda12-8077-4c82-a2aa-10ba512fcc75", + "name": "Fox News HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/f2134c41-965e-4ffa-9d0b-3a8fb0b576e7.m3u8", + "epg_source_url": "https://epg.pw/last/465372.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoxNewsChannel.us.png", + "created_at": "2026-03-21T02:53:51.758717+00:00", + "updated_at": "2026-05-29T05:35:55.203506+00:00" + }, + { + "id": "4567e88a-0e47-4e61-ab50-6d9ff92252e2", + "name": "Fox Sports 1 HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/124a2e29-8677-48cc-b8be-2b00f748e497.m3u8", + "epg_source_url": "https://epg.pw/last/465291.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoxSports1.us.png", + "created_at": "2026-03-21T02:55:22.886779+00:00", + "updated_at": "2026-05-29T07:00:49.10867+00:00" + }, + { + "id": "22483c87-bd9c-4880-8970-98a532680d40", + "name": "Fox Sports 2 HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/d1f8a09a-0c14-4445-a3a4-9b27982da2fb.m3u8", + "epg_source_url": "https://epg.pw/last/465355.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FoxSports2.us.png", + "created_at": "2026-03-21T02:55:59.774207+00:00", + "updated_at": "2026-05-29T07:03:35.704252+00:00" + }, + { + "id": "3118f2f8-b956-4214-9c58-b07e78a06b21", + "name": "FreeForm HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/e7af2de6-2703-47dd-9af1-10b3048b1e9d.m3u8", + "epg_source_url": "https://epg.pw/last/465331.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Freeform.us.png", + "created_at": "2026-03-21T02:56:52.600199+00:00", + "updated_at": "2026-05-29T07:05:56.144856+00:00" + }, + { + "id": "08369fe5-f318-4a89-982d-9f63c537302e", + "name": "FX HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/8a3432fb-04a7-4ade-a28f-5c1a0cc3f68e.m3u8", + "epg_source_url": "https://epg.pw/last/465400.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FX.us.png", + "created_at": "2026-03-21T02:58:31.867323+00:00", + "updated_at": "2026-05-31T04:12:55.759989+00:00" + }, + { + "id": "da593975-ed36-41eb-889d-defc075ac64a", + "name": "Hallmark Family HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/2fab5943-6302-4d69-beaf-57c64e65a3d5.m3u8", + "epg_source_url": "https://epg.pw/last/464897.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HallmarkFamily.us.png", + "created_at": "2026-03-21T02:59:20.6632+00:00", + "updated_at": "2026-05-31T04:14:47.223369+00:00" + }, + { + "id": "19395309-c667-409d-9104-3196557a6c2a", + "name": "HBO 2 HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/af8aa9b5-ddaf-483f-bbbc-34bc24d180c1.m3u8", + "epg_source_url": "https://epg.pw/last/465041.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HBO2.us.png", + "created_at": "2026-03-21T03:02:20.451424+00:00", + "updated_at": "2026-05-31T04:16:00.039049+00:00" + }, + { + "id": "f9a9b988-868e-4014-8bcd-fe32da862d39", + "name": "HBO Family HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/4024db2b-e32f-49e2-936c-db299b7b3c70.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HBOFamily.us.png", + "created_at": "2026-03-21T03:06:01.005499+00:00", + "updated_at": "2026-05-31T04:17:20.507524+00:00" + }, + { + "id": "4499e99d-8e5b-4087-9d51-9c61a820ca02", + "name": "HBO HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/40b05b79-ee37-4d80-81be-fecb99539003.m3u8", + "epg_source_url": "https://epg.pw/last/464745.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HBO.us.png", + "created_at": "2026-03-21T03:06:42.119801+00:00", + "updated_at": "2026-06-01T01:21:54.617392+00:00" + }, + { + "id": "fabb4415-d644-44dd-b083-dab8c77d8833", + "name": "HBO Latino HD | USA", + "category": "movies", + "source_url": "v", + "epg_source_url": "https://epg.pw/last/464960.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HBOLatino.us.png", + "created_at": "2026-03-21T03:07:23.376546+00:00", + "updated_at": "2026-05-29T07:15:46.889441+00:00" + }, + { + "id": "8cf4517b-a7c4-4e97-b2de-7ff3ada11543", + "name": "HBO Signature HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/51d76cab-aff4-435a-ae57-88c108092ba7.m3u8", + "epg_source_url": "https://epg.pw/last/465279.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HBOSignature.us.png", + "created_at": "2026-03-21T03:08:02.732725+00:00", + "updated_at": "2026-05-31T04:21:39.827678+00:00" + }, + { + "id": "72b7a1df-54ef-4ca1-9c82-e526977775c5", + "name": "HGTV HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/6ed802f7-51b2-459d-8f90-4198bc46f61f.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/HGTV.us.png", + "created_at": "2026-03-21T03:09:02.609226+00:00", + "updated_at": "2026-05-29T07:17:45.87031+00:00" + }, + { + "id": "6f629954-45bb-4263-a58e-ba47f9277ef5", + "name": "History HD | USA", + "category": "documentary", + "source_url": "https://hey.dulo.tv/memfs/5fb5f1b0-c84e-413d-aa65-fe96d832f1ac.m3u8", + "epg_source_url": "https://epg.pw/last/465176.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/History.us.png", + "created_at": "2026-03-21T03:09:32.616062+00:00", + "updated_at": "2026-05-31T04:07:59.946592+00:00" + }, + { + "id": "50fea76c-3c5b-45b9-a8c2-31093fd6541b", + "name": "Investigation Discovery HD | USA", + "category": "documentary", + "source_url": "https://hey.dulo.tv/memfs/cc30205b-d5b7-47ca-bf66-784f4dbe9bdd.m3u8", + "epg_source_url": "https://epg.pw/last/465294.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/InvestigationDiscovery.us.png", + "created_at": "2026-03-21T03:10:17.256111+00:00", + "updated_at": "2026-05-31T04:09:42.485452+00:00" + }, + { + "id": "dc23c106-df27-4364-9dcc-7d3a30ad44e2", + "name": "Lifetime HD | TV", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/c00b0f90-4a1b-4846-b676-c2eeff8a01bf.m3u8", + "epg_source_url": "https://epg.pw/last/465290.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Lifetime.us.png", + "created_at": "2026-03-21T03:11:13.848025+00:00", + "updated_at": "2026-05-29T07:22:30.630607+00:00" + }, + { + "id": "d0bc8a90-1bd2-4e7f-8875-0b2be40888f8", + "name": "Magnolia Network HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/0a16d94a-fda9-4d63-bb7a-c7003f7336c8.m3u8", + "epg_source_url": "https://epg.pw/last/465012.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/MagnoliaNetwork.us.png", + "created_at": "2026-03-21T03:11:51.707394+00:00", + "updated_at": "2026-05-29T07:21:16.53881+00:00" + }, + { + "id": "88242062-2bfd-4547-9043-b2189399f43e", + "name": "MLB Network HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/a185d75f-7635-4b51-bb42-e6d776917d14.m3u8", + "epg_source_url": "https://epg.pw/last/465239.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/MLBNetwork.us.png", + "created_at": "2026-03-21T03:18:32.884349+00:00", + "updated_at": "2026-05-29T07:23:35.949174+00:00" + }, + { + "id": "ae1bbb1c-fce3-4768-b13a-1417b27e0c76", + "name": "MSG HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/84239837-d11a-43d7-a645-923e1554e7ba.m3u8", + "epg_source_url": "https://epg.pw/last/464996.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/MSG.us.png", + "created_at": "2026-03-21T03:19:11.991924+00:00", + "updated_at": "2026-05-29T07:24:50.957453+00:00" + }, + { + "id": "a3d24518-0ec0-4a60-bb74-43e62f923277", + "name": "MSNBC HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/e77e70b2-f513-4f5c-ba45-42743dbc5453.m3u8", + "epg_source_url": "https://epg.pw/last/465087.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/MSNBC.us.png", + "created_at": "2026-03-21T03:19:51.602647+00:00", + "updated_at": "2026-05-29T07:25:48.980452+00:00" + }, + { + "id": "96d5474d-facb-48f0-875e-6e1836ecf5c2", + "name": "MTV HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/4484de6d-491b-4a3d-967f-08e68b63b174.m3u8", + "epg_source_url": "https://epg.pw/last/464997.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/MTV.us.png", + "created_at": "2026-03-21T03:20:42.8248+00:00", + "updated_at": "2026-05-29T07:26:58.397976+00:00" + }, + { + "id": "2798d6d7-b2e5-46f0-8753-7be9c3cfcf22", + "name": "National Geographic HD | USA", + "category": "documentary", + "source_url": "https://hey.dulo.tv/memfs/a9835c70-a51d-4e4a-a001-6ffc18dcc9ba.m3u8", + "epg_source_url": "https://epg.pw/last/465089.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NationalGeographic.us.png", + "created_at": "2026-03-21T03:22:45.607231+00:00", + "updated_at": "2026-05-31T04:10:59.725608+00:00" + }, + { + "id": "2d707c68-d53a-4174-9b3a-78e44b300e63", + "name": "NBA TV HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/0d6106f5-50e4-43f1-8ab5-431dac6fc4c0.m3u8", + "epg_source_url": "https://epg.pw/last/464912.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NBAtv.us.png", + "created_at": "2026-03-21T03:23:17.032514+00:00", + "updated_at": "2026-05-29T07:28:53.544655+00:00" + }, + { + "id": "671a4cce-efa5-4254-a2fc-d17278e3ded3", + "name": "NESN HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/8bd363bf-6fb2-4315-ac1d-1c1952f7f4cd.m3u8", + "epg_source_url": "https://epg.pw/last/496625.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NESN.us.png", + "created_at": "2026-03-21T03:24:12.362053+00:00", + "updated_at": "2026-05-29T07:29:45.835839+00:00" + }, + { + "id": "312c8261-c482-4346-8437-83eb9436c91b", + "name": "NFL Network HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/15816015-c1d0-47da-81e9-ae68e9d2b67f.m3u8", + "epg_source_url": "https://epg.pw/last/465311.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NFLNetwork.us.png", + "created_at": "2026-03-21T03:25:18.812068+00:00", + "updated_at": "2026-05-29T07:30:58.034779+00:00" + }, + { + "id": "1e89de9c-999b-4193-8414-a444631e3728", + "name": "NHL Network HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/4546b90f-28a7-4f12-88c3-856df043e237.m3u8", + "epg_source_url": "https://epg.pw/last/465348.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NHLNetwork.us.png", + "created_at": "2026-03-21T03:25:52.205833+00:00", + "updated_at": "2026-05-29T07:31:50.372489+00:00" + }, + { + "id": "6b75e415-e3a5-4815-9cf2-237e7f11ae2e", + "name": "Nickelodeon East HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/59c180f4-1bdc-488c-8e29-9e051f477392.m3u8", + "epg_source_url": "https://epg.pw/last/465251.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Nickelodeon.us.png", + "created_at": "2026-03-21T03:26:51.363775+00:00", + "updated_at": "2026-05-31T04:00:33.085061+00:00" + }, + { + "id": "861952e3-3157-4635-9944-d2d074a817ab", + "name": "Nicktoons HD | USA", + "category": "kids", + "source_url": "https://hey.dulo.tv/memfs/4108a8ad-602e-4a25-9f71-21cd1338a403.m3u8", + "epg_source_url": "https://epg.pw/last/465038.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Nicktoons.us.png", + "created_at": "2026-03-21T03:27:22.543312+00:00", + "updated_at": "2026-05-31T04:02:04.2053+00:00" + }, + { + "id": "27dfd93f-08da-4a65-8d9f-b174f7062d79", + "name": "OWN HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/ca273200-b240-48cc-b03a-31da3e2e7d75.m3u8", + "epg_source_url": "https://epg.pw/last/465259.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/OWN.us.png", + "created_at": "2026-03-21T03:28:06.545349+00:00", + "updated_at": "2026-05-29T07:35:37.181278+00:00" + }, + { + "id": "be1f5f0e-80c8-4fb1-a2ff-fe6a6e55e2de", + "name": "Paramount Network HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/e8226677-87ba-4f1f-ba23-7b992c6336fb.m3u8", + "epg_source_url": "https://epg.pw/last/464882.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ParamountNetwork.us.png", + "created_at": "2026-03-21T03:28:38.876193+00:00", + "updated_at": "2026-05-29T07:36:55.489839+00:00" + }, + { + "id": "116bab23-7ece-4230-94d1-796631e244c5", + "name": "Showtime Extreme HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/ba77ea81-a931-48aa-964e-6536852f60cb.m3u8", + "epg_source_url": "https://epg.pw/last/464877.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/ShowtimeExtreme.us.png", + "created_at": "2026-03-21T03:30:39.298218+00:00", + "updated_at": "2026-05-31T04:24:31.087636+00:00" + }, + { + "id": "62f075b0-cdd3-4fdb-b0e9-e32c49285836", + "name": "Sony Movie Channel HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/75ddfc2c-aee3-4fd2-a70d-d368b1e44736.m3u8", + "epg_source_url": "https://epg.pw/last/464943.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/SonyMovieChannel.us.png", + "created_at": "2026-03-21T03:37:54.799048+00:00", + "updated_at": "2026-05-31T04:28:30.797998+00:00" + }, + { + "id": "b027a9ab-e147-405e-b77c-868f2f225300", + "name": "Spectrum SportsNet LA Dodgers HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/c12d46a2-a58e-4dd8-a178-533365efc760.m3u8", + "epg_source_url": "https://epg.pw/last/465077.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/SpectrumSportsNetLADodgers.us.png", + "created_at": "2026-03-21T03:39:06.913655+00:00", + "updated_at": "2026-05-29T07:42:48.918011+00:00" + }, + { + "id": "5c83b496-df9e-4dae-9554-d93cd482177a", + "name": "Starz Cinema HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/2df70247-3213-47e3-8553-bc8c28d0c67f.m3u8", + "epg_source_url": "https://epg.pw/last/465115.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/StarzCinema.us.png", + "created_at": "2026-03-21T03:40:15.481353+00:00", + "updated_at": "2026-05-31T04:29:37.115273+00:00" + }, + { + "id": "667683d0-e7d7-4f10-af12-5174ee74d8ed", + "name": "Starz East HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/8789fb20-a653-465f-b491-937724e3765b.m3u8", + "epg_source_url": "https://epg.pw/last/464975.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Starz.us.png", + "created_at": "2026-03-21T03:40:41.308802+00:00", + "updated_at": "2026-05-31T04:30:59.041617+00:00" + }, + { + "id": "126761fd-9c50-46d7-9821-a7503528fe7f", + "name": "Starz Encore HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/5e336b09-a46c-4a0f-b72f-886c30dc92cb.m3u8", + "epg_source_url": "https://epg.pw/last/464893.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/StarzEncore.us.png", + "created_at": "2026-03-21T03:41:06.243462+00:00", + "updated_at": "2026-05-31T04:32:13.702714+00:00" + }, + { + "id": "11d8bbd6-b65b-4138-ac22-0194346db38e", + "name": "SyFy HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/010d6d1d-a8ed-42dc-abc5-82124ede2a59.m3u8", + "epg_source_url": "https://epg.pw/last/465053.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/Syfy.us.png", + "created_at": "2026-03-21T03:41:42.96235+00:00", + "updated_at": "2026-05-31T04:34:35.485667+00:00" + }, + { + "id": "b1428e38-1dd2-41ef-94b6-f2f24b435a63", + "name": "Tennis Channel HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/632c60eb-7798-4695-b617-0271e22b47d1.m3u8", + "epg_source_url": "https://epg.pw/last/465236.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/TennisChannel.us.png", + "created_at": "2026-03-21T03:42:21.39763+00:00", + "updated_at": "2026-05-29T07:51:03.463954+00:00" + }, + { + "id": "5d135d5f-23f5-4c77-881e-dd074ad1844e", + "name": "The Weather Channel HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/faa13ade-5cc2-451a-86b2-b22de56da424.m3u8", + "epg_source_url": "https://epg.pw/last/465272.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/TheWeatherChannel.us.png", + "created_at": "2026-03-21T03:43:01.773063+00:00", + "updated_at": "2026-05-29T07:53:17.45128+00:00" + }, + { + "id": "ac80172e-e8ff-4a06-b956-bc25bf3b519f", + "name": "TNT HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/875059b4-6cfa-4be2-9e86-159be43583e8.m3u8", + "epg_source_url": "https://epg.pw/last/465114.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/TNT.us.png", + "created_at": "2026-03-21T03:43:36.092544+00:00", + "updated_at": "2026-05-29T07:54:14.862904+00:00" + }, + { + "id": "f822a7fa-ed38-4a95-aa8f-69ad98fcbce7", + "name": "TruTV HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/2965b183-a648-4866-b598-3e193785d33f.m3u8", + "epg_source_url": "https://epg.pw/last/464987.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/truTV.us.png", + "created_at": "2026-03-21T03:44:42.470318+00:00", + "updated_at": "2026-05-29T07:55:23.124502+00:00" + }, + { + "id": "74d709fa-01e7-4499-bc3e-d1a6b3540948", + "name": "USA Network HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/744d72b2-0d9c-45c0-9824-6f030ad06ad3.m3u8", + "epg_source_url": "https://epg.pw/last/465006.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/USANetwork.us.png", + "created_at": "2026-03-21T03:45:16.676066+00:00", + "updated_at": "2026-05-29T07:56:09.163848+00:00" + }, + { + "id": "344d07b6-4b72-4aae-94ce-c1a508167d3d", + "name": "VH1 HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/34ae4c6e-82c1-4ef4-b871-ac82bf960154.m3u8", + "epg_source_url": "https://epg.pw/last/465206.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/VH1.us.png", + "created_at": "2026-03-21T03:46:00.402281+00:00", + "updated_at": "2026-05-29T07:57:09.238706+00:00" + }, + { + "id": "c7e4a13a-142f-433c-8fe3-55cbae1e67d5", + "name": "Willow Cricket HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/b66967fb-5afe-4439-a846-01b14ff2f65a.m3u8", + "epg_source_url": "https://epg.pw/last/465010.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/WillowCricket.us.png", + "created_at": "2026-03-21T03:46:35.98409+00:00", + "updated_at": "2026-05-29T07:57:55.395224+00:00" + }, + { + "id": "08fa49e0-4f5c-4a10-9f42-4c0249be664b", + "name": "Sky Sports F1 HD | UK", + "category": "sports", + "source_url": "https://hey.dulo.tv/memfs/d43aa050-1751-4711-8ed7-1b434031aa7c.m3u8", + "epg_source_url": "https://epg.pw/last/412947.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsF1.uk.png", + "created_at": "2026-03-21T04:21:48.661475+00:00", + "updated_at": "2026-05-31T04:55:19.446477+00:00" + }, + { + "id": "969ede28-9cc2-4c65-9454-20fc37d0e2b8", + "name": "Sky Sports Main Event HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/287d1543-ec5e-49c1-b492-01eb358d1ad5.m3u8", + "epg_source_url": "https://epg.pw/last/7673.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsMainEvent.uk.png", + "created_at": "2026-03-21T04:22:22.419164+00:00", + "updated_at": "2026-04-10T15:52:08.818197+00:00" + }, + { + "id": "36c4d9d1-4d43-45f7-8477-c1bfbe0ad217", + "name": "Sportsnet East HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/9105ba79-7a42-47e2-b344-3d07c6d0aef9.m3u8", + "epg_source_url": "https://epg.pw/last/470812.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/SportsnetEast.ca.png", + "created_at": "2026-03-21T04:27:41.947382+00:00", + "updated_at": "2026-04-10T15:52:26.542449+00:00" + }, + { + "id": "688ddade-4c5f-4b1a-9fe3-b3565475b5dd", + "name": "TSN 1 HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/3929afa8-022d-4bda-af5c-aa39c33b05de.m3u8", + "epg_source_url": "https://epg.pw/last/470446.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/TSN1.ca.png", + "created_at": "2026-03-21T04:28:36.295412+00:00", + "updated_at": "2026-05-29T08:04:59.296329+00:00" + }, + { + "id": "e7756305-b952-456a-8aa8-8253fcb77209", + "name": "TSN 2 HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/79e4cfd2-676b-48de-b035-54f5ef7dfa28.m3u8", + "epg_source_url": "https://epg.pw/last/470550.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/TSN2.ca.png", + "created_at": "2026-03-21T04:31:08.698696+00:00", + "updated_at": "2026-05-29T08:06:06.13564+00:00" + }, + { + "id": "cf99e9c5-9cb8-41a9-81fd-1e27e56ecead", + "name": "TSN 3 HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/8176f064-c2d0-4cba-9899-c533e80d68a8.m3u8", + "epg_source_url": "https://epg.pw/last/470888.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/TSN3.ca.png", + "created_at": "2026-03-21T04:31:59.186974+00:00", + "updated_at": "2026-05-29T08:07:13.796767+00:00" + }, + { + "id": "83fd2f2e-249f-43b4-b09b-dc9de11137f9", + "name": "GSN HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/d8f0da98-667f-4ff1-b090-79ec3df30a97.m3u8", + "epg_source_url": "https://epg.pw/last/464810.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/GameShowNetworkPacific.us.png", + "created_at": "2026-03-24T20:00:02.586783+00:00", + "updated_at": "2026-05-29T08:08:31.4082+00:00" + }, + { + "id": "c4dda23a-c202-4e2c-8be8-f5ed5e748534", + "name": "TNT Sports 1 HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/e773710d-d3e5-4e03-8470-3a6070cf9285.m3u8", + "epg_source_url": "https://epg.pw/last/400477.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/TNTSports1.uk.png", + "created_at": "2026-03-25T00:36:12.494905+00:00", + "updated_at": "2026-05-29T08:09:26.192475+00:00" + }, + { + "id": "7f37f6cb-d5c2-4003-87e2-780c942d1f8d", + "name": "TNT Sports 2 HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/4297a668-7c2f-48dd-b6f3-d07d539c3b26.m3u8", + "epg_source_url": "https://epg.pw/last/400480.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/TNTSports2.uk.png", + "created_at": "2026-03-25T00:43:02.010045+00:00", + "updated_at": "2026-05-29T08:10:01.685651+00:00" + }, + { + "id": "e1d60af0-164b-41bf-abd4-1710aed0911b", + "name": "NBC Sports Bay Area HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/713cb02d-2bb3-4259-9bb1-f1b8174a16a1.m3u8", + "epg_source_url": "https://epg.pw/last/465163.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NBCSportsBayArea.us.png", + "created_at": "2026-03-26T17:43:41.261854+00:00", + "updated_at": "2026-05-29T08:11:10.230781+00:00" + }, + { + "id": "682e3578-7e54-4e67-b856-79052ee22d8e", + "name": "NBC Sports California HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/153aceee-ff14-495c-8620-9cfe9e9e505b.m3u8", + "epg_source_url": "https://epg.pw/last/465054.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NBCSportsCalifornia.us.png", + "created_at": "2026-03-26T17:50:35.09763+00:00", + "updated_at": "2026-05-29T08:12:08.901538+00:00" + }, + { + "id": "a4c38821-8716-4e9e-b755-9524fbae8886", + "name": "Travel Channel HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/6642f732-1ad7-4273-b30a-d12cd639f99c.m3u8", + "epg_source_url": "https://epg.pw/last/465186.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/TravelChannel.us.png", + "created_at": "2026-03-26T18:19:46.814735+00:00", + "updated_at": "2026-05-29T08:13:22.968269+00:00" + }, + { + "id": "84522924-f79c-4cd4-bb22-df9f19502ce7", + "name": "Destination America HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/685d35e8-8e90-4ceb-9241-a18a17056b34.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/DestinationAmerica.us.png", + "created_at": "2026-03-26T18:22:32.872215+00:00", + "updated_at": "2026-05-29T08:14:21.066497+00:00" + }, + { + "id": "a06ea1dd-0304-4325-9f15-5f5e6969849f", + "name": "fyi HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/4fd9553b-efc3-4b15-8c81-c27d861fa94d.m3u8", + "epg_source_url": "https://epg.pw/last/464774.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/FYIChannel.us.png", + "created_at": "2026-03-26T18:26:25.132348+00:00", + "updated_at": "2026-05-29T08:15:17.406884+00:00" + }, + { + "id": "df2cc909-cdda-4061-bebe-b31d210fa62c", + "name": "NBC 4 HD | US", + "category": "news", + "source_url": "https://hey.dulo.tv/memfs/03e8462d-5cf5-4bca-b134-125e228e5e1d.m3u8", + "epg_source_url": "https://epg.pw/last/467015.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/NBCWNBC.us.png", + "created_at": "2026-03-26T18:31:17.645806+00:00", + "updated_at": "2026-05-31T04:44:29.974662+00:00" + }, + { + "id": "4f3ddbdb-38ea-4853-8fad-404f0899b1dd", + "name": "CBS 2 NY HD | USA", + "category": "news", + "source_url": "https://images.dulo.tv/memfs/bbf679fc-bf77-4ac4-a988-b36195bd1e31.m3u8", + "epg_source_url": "https://epg.pw/last/468311.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/WCBS.png", + "created_at": "2026-03-26T18:33:45.721679+00:00", + "updated_at": "2026-05-29T08:17:35.374314+00:00" + }, + { + "id": "0e5da8bf-5203-4abf-8dd1-ed7e28b33f10", + "name": "Smithsonian Channel HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/3c03d782-4a10-43e4-8209-b23ee2759e88.m3u8", + "epg_source_url": "https://epg.pw/last/465042.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/smithsonianchannel.png", + "created_at": "2026-03-26T18:40:30.072842+00:00", + "updated_at": "2026-05-29T08:18:37.144144+00:00" + }, + { + "id": "5e675be4-89a2-43d7-8917-3c6e78acc30b", + "name": "TSN 4 HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/7c780698-133a-48ef-bdbf-585c4b7ab6c9.m3u8", + "epg_source_url": "https://epg.pw/last/470730.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/TSN4.ca.png", + "created_at": "2026-04-07T21:21:50.538815+00:00", + "updated_at": "2026-05-29T08:19:43.457672+00:00" + }, + { + "id": "aaf601ce-6337-4f14-ac22-e2118f49072c", + "name": "TSN 5 HD | CA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/48a842fe-0f1d-4daf-ad63-c930ac09d1bf.m3u8", + "epg_source_url": "https://epg.pw/last/470390.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/Canada/TSN5.ca.png", + "created_at": "2026-04-07T21:23:40.840324+00:00", + "updated_at": "2026-05-29T08:20:29.565861+00:00" + }, + { + "id": "98a9cd29-0a98-4cb1-8334-18067b9d47ac", + "name": "CBS Sports HD | USA", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/46ea23e8-f030-41d4-a5f9-19cb3c5adc92.m3u8", + "epg_source_url": "https://epg.pw/last/464937.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CBSSportsNetwork.us.png", + "created_at": "2026-04-07T21:50:54.367966+00:00", + "updated_at": "2026-05-29T08:21:58.704388+00:00" + }, + { + "id": "94140129-0daf-4343-ab07-50fee471c779", + "name": "TBS HD | USA", + "category": "entertainment", + "source_url": "https://images.dulo.tv/memfs/cffea07a-4d5c-4764-8361-126aa6d28c84.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/TBS.us.png", + "created_at": "2026-05-02T19:41:54.342709+00:00", + "updated_at": "2026-05-29T08:22:54.580611+00:00" + }, + { + "id": "25e200e4-c52b-4a7c-9c17-88665920a5d4", + "name": "Sky Sports Football HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/918682c9-4b60-475f-a2ef-f619532e8fc1.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsFootball.uk.png", + "created_at": "2026-05-06T09:18:40.50746+00:00", + "updated_at": "2026-05-30T15:20:40.980869+00:00" + }, + { + "id": "ab1b856c-bec7-4520-9d70-ac3f6a6a9d36", + "name": "Sky Sports Mix HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/80c0df20-2f36-4ed7-8a34-5530a3c833fd.m3u8", + "epg_source_url": "https://epg.pw/last/6553.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsMix.uk.png", + "created_at": "2026-05-06T09:20:11.722544+00:00", + "updated_at": "2026-05-29T08:26:17.955329+00:00" + }, + { + "id": "4403220d-a412-45fa-8458-6dfac6894a6c", + "name": "Sky Sports Main Event HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/53ff8ded-f296-46ce-a676-fccb8b839ba8.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsMainEvent.uk.png", + "created_at": "2026-05-06T09:21:54.036345+00:00", + "updated_at": "2026-05-29T08:27:33.143056+00:00" + }, + { + "id": "8476925a-1dc5-4d8b-b69f-afa073a341ad", + "name": "Sky Sports Cricket HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/00252d98-9fe9-4275-98b0-c548d029caf5.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsCricket.uk.png", + "created_at": "2026-05-06T09:30:08.06863+00:00", + "updated_at": "2026-05-29T08:28:36.722144+00:00" + }, + { + "id": "bb70e26a-4569-4fe3-8b10-9db4a8367933", + "name": "Sky Sports Golf HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/b646bb4b-a79f-460a-af78-57dd5ee3e42a.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsGolf.uk.png", + "created_at": "2026-05-06T09:32:12.119012+00:00", + "updated_at": "2026-05-29T08:29:46.486484+00:00" + }, + { + "id": "00313f16-3d50-428b-bae6-5f7d7e30de2d", + "name": "Sky Sports Premier League HD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/7e585fac-7375-4953-b749-1e1610e84f8c.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsPremierLeague.uk.png", + "created_at": "2026-05-06T09:35:09.326329+00:00", + "updated_at": "2026-05-29T08:30:54.868654+00:00" + }, + { + "id": "9050e056-6c10-4f86-9a75-a65fe8598e01", + "name": "Star Sports 1 FHD | IND", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/eca1b43d-cbc1-4f71-b26d-c4add1decc54.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/India/StarSports1.in.png", + "created_at": "2026-05-06T13:27:55.894929+00:00", + "updated_at": "2026-05-29T08:39:40.19523+00:00" + }, + { + "id": "57e316ac-f55a-4d98-93da-5ca698875913", + "name": "TNT Sports 1 HD Backup | UK", + "category": "sports", + "source_url": "https://cdn.dulo.tv/memfs/c015ed02-bdbd-471d-946a-c56646b47c62.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/TNTSports1.uk.png", + "created_at": "2026-05-06T20:37:20.144135+00:00", + "updated_at": "2026-05-22T21:28:59.374074+00:00" + }, + { + "id": "1c511cd8-ed3c-4a98-82e0-2bb9ebe8afe9", + "name": "Sky Sports Main Event SD | UK", + "category": "sports", + "source_url": "https://images.dulo.tv/memfs/c837ef60-8e7d-4cb6-8748-a7c720e83f8f.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/UK/SkySportsMainEvent.uk.png", + "created_at": "2026-05-16T13:07:32.818408+00:00", + "updated_at": "2026-05-16T13:07:58.89855+00:00" + }, + { + "id": "8c60ce75-1f0d-41e7-997e-1e71444871f4", + "name": "Starz Encore Espanol HD | Latino", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/5d4352d8-0e6e-465c-b65d-46adaaa69885.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://cdn.iptvboss.pro/logos/USA/StarzEncoreEspanol.us.png", + "created_at": "2026-05-20T02:08:19.805916+00:00", + "updated_at": "2026-05-31T04:35:58.163507+00:00" + }, + { + "id": "95f88d4a-7349-4aec-b62c-06baaea70460", + "name": "ESPN Deportes HD | Latino", + "category": "sports", + "source_url": "https://bridge.dulo.tv/memfs/cd89307e-34c7-4efc-9e23-c6f1f60422ee.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://cdn.iptvboss.pro/logos/USA/ESPNDeportes.us.png", + "created_at": "2026-05-20T02:21:40.559962+00:00", + "updated_at": "2026-05-26T01:47:38.901139+00:00" + }, + { + "id": "2af62785-0139-499a-8f4d-ac35b702ef06", + "name": "24/7 Peppa Pig", + "category": "kids", + "source_url": "https://bridge.dulo.tv/memfs/754b034f-6353-46a9-842e-4f717ec5d194.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://logos-world.net/wp-content/uploads/2024/01/Peppa-Pig-Logo-500x281.png", + "created_at": "2026-05-20T02:41:58.640365+00:00", + "updated_at": "2026-05-26T01:46:26.528306+00:00" + }, + { + "id": "81a1ebb1-ba3e-4b13-a2a0-b6027575a815", + "name": "24/7 SpongeBob SquarePants", + "category": "kids", + "source_url": "https://bridge.dulo.tv/memfs/a2e386d8-c6f0-4e9e-ba66-b232195f45f9.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://1000logos.net/wp-content/uploads/2020/09/SpongeBob_SquarePants_logo.png", + "created_at": "2026-05-20T02:43:59.742145+00:00", + "updated_at": "2026-05-26T01:43:54.94617+00:00" + }, + { + "id": "8c6bb9f3-548f-4e98-9ced-56178bb0beb3", + "name": "24/7 Bluey", + "category": "kids", + "source_url": "https://bridge.dulo.tv/memfs/79bcd103-4636-4a23-9372-f2a58fa49288.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://media.printables.com/media/prints/d35fce99-be65-436d-ae66-a833cc1a0754/images/10303645_a3372cce-e5aa-4679-9d42-52c1387e273c_0037f94f-e35b-498c-8100-1698b420d1fa/thumbs/cover/1200x630/png/bluey-logo.png", + "created_at": "2026-05-20T02:45:51.398708+00:00", + "updated_at": "2026-05-26T01:45:14.956531+00:00" + }, + { + "id": "c5b8ec5e-bec1-4632-9dff-8bf1cb33b655", + "name": "Cinemax HD | USA", + "category": "movies", + "source_url": "https://hey.dulo.tv/memfs/b0bfb3af-3b3a-49bc-ace2-a0a1bdfb38e9.m3u8", + "epg_source_url": null, + "direct_source": true, + "sort_order": 0, + "logo_url": "https://iptvboss.xyz/logos/USA/CineMAX.us.png", + "created_at": "2026-05-20T16:32:20.354786+00:00", + "updated_at": "2026-05-31T04:37:54.058853+00:00" + }, + { + "id": "8c4e39a2-745c-4bc5-a2da-a65cf91f8214", + "name": "Chicago Sports Network HD | USA", + "category": "sports", + "source_url": "https://bridge.dulo.tv/memfs/a45a9917-b8e0-4473-bb0e-1ec2377be9d7.m3u8", + "epg_source_url": "https://epg.pw/last/465058.html?lang=en", + "direct_source": true, + "sort_order": 0, + "logo_url": "https://cdn.iptvboss.pro/logos/USA/ChicagoSportsNetwork.us.png", + "created_at": "2026-05-29T23:43:02.847366+00:00", + "updated_at": "2026-05-29T23:43:42.123003+00:00" + } + ] +} \ No newline at end of file diff --git a/server/seed-data/sources/dulo.source.json b/server/seed-data/sources/dulo.source.json new file mode 100644 index 00000000..5b055eda --- /dev/null +++ b/server/seed-data/sources/dulo.source.json @@ -0,0 +1,1532 @@ +[ + { + "_id": "dulo:0b56be49-557e-4315-9258-e9a0728ab608", + "source": "dulo", + "sourceChannelId": "0b56be49-557e-4315-9258-e9a0728ab608", + "name": "A&E East HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/AandENetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/2afff205-c2ef-4621-a716-62e65d313b5c.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:29:23.917Z", + "sourceUpdatedAt": "2026-06-09T01:37:10.561Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:5e5ba7f3-6967-4504-9f26-b9f17ec42ead", + "source": "dulo", + "sourceChannelId": "5e5ba7f3-6967-4504-9f26-b9f17ec42ead", + "name": "ABC HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/ABC%20%28copy%29.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/cf396710-6c4d-4c85-9021-df9c24fd6c0b.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:30:00.439Z", + "sourceUpdatedAt": "2026-05-29T05:39:06.728Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e2ce7dba-a5e5-4b5c-a935-341f4a08ddeb", + "source": "dulo", + "sourceChannelId": "e2ce7dba-a5e5-4b5c-a935-341f4a08ddeb", + "name": "ACC Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ACCNetwork.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/a2a7afae-9bc8-410f-8383-eb1e4117d2f9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:30:40.651Z", + "sourceUpdatedAt": "2026-05-31T17:47:37.753Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:afa5347e-e2ab-403c-8670-61f8c4fd47f1", + "source": "dulo", + "sourceChannelId": "afa5347e-e2ab-403c-8670-61f8c4fd47f1", + "name": "Al Jazeera HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/AlJazeera.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/bd712599-b656-49a7-9613-a60e8615bdf4.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:31:24.750Z", + "sourceUpdatedAt": "2026-05-29T05:41:06.853Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e06e04f5-6e51-42f8-a3c0-7f7bcadb50be", + "source": "dulo", + "sourceChannelId": "e06e04f5-6e51-42f8-a3c0-7f7bcadb50be", + "name": "AMC HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/AMC.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/e60d4c96-718b-4d71-85f6-8787033cc435.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:32:01.414Z", + "sourceUpdatedAt": "2026-06-09T01:22:25.514Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:cd124020-4cc8-460f-b730-e0393015d164", + "source": "dulo", + "sourceChannelId": "cd124020-4cc8-460f-b730-e0393015d164", + "name": "Animal Planet HD | USA", + "category": "documentary", + "groupKey": "documentary", + "groupLabel": "documentary", + "logoUrl": "https://iptvboss.xyz/logos/USA/AnimalPlanet.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/990a73ff-81d2-4222-92d0-6d1b4330245c.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:32:41.748Z", + "sourceUpdatedAt": "2026-05-31T04:05:36.889Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:bd829da5-d754-4e02-bdf5-65a14e666c43", + "source": "dulo", + "sourceChannelId": "bd829da5-d754-4e02-bdf5-65a14e666c43", + "name": "AT&T SportsNet Pittsburgh HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ATTSportsNetPittsburgh.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/8cf8ced1-f653-4819-96ee-a25b182b2737.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:33:30.494Z", + "sourceUpdatedAt": "2026-06-09T05:02:01.773Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:35427165-289d-43b6-82bc-833c2e6e1341", + "source": "dulo", + "sourceChannelId": "35427165-289d-43b6-82bc-833c2e6e1341", + "name": "beIN Sports HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/beINSports.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/4631454f-f671-40f1-b766-6bed57ffec18.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:34:09.504Z", + "sourceUpdatedAt": "2026-06-09T05:04:14.176Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:a38ac31c-23fd-42f1-aa43-a5a30a2d7554", + "source": "dulo", + "sourceChannelId": "a38ac31c-23fd-42f1-aa43-a5a30a2d7554", + "name": "BET HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/BET.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/b8299a0d-07ec-40cb-b997-201434cbfefa.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:35:28.778Z", + "sourceUpdatedAt": "2026-06-09T01:23:14.374Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:15be82f9-7d1c-4035-8132-afcefe933063", + "source": "dulo", + "sourceChannelId": "15be82f9-7d1c-4035-8132-afcefe933063", + "name": "Big Ten Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/BigTen.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/b8299a0d-07ec-40cb-b997-201434cbfefa.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:36:15.830Z", + "sourceUpdatedAt": "2026-06-09T01:23:30.071Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e18e2a96-41e4-4638-8402-f46ada39272d", + "source": "dulo", + "sourceChannelId": "e18e2a96-41e4-4638-8402-f46ada39272d", + "name": "Boomerang HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/Boomerang.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/b48d96c2-dd3c-4a6c-a5ae-41bf2220a61e.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:36:52.170Z", + "sourceUpdatedAt": "2026-05-31T04:03:25.431Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:9149b685-782f-4560-941a-7624d0857552", + "source": "dulo", + "sourceChannelId": "9149b685-782f-4560-941a-7624d0857552", + "name": "Bravo East HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/Bravo.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/ffe47a87-9e1f-4539-9879-e2fae54d3930.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:37:32.707Z", + "sourceUpdatedAt": "2026-06-09T01:26:00.849Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e4e962a7-7d7c-4361-a271-fc6c52b19466", + "source": "dulo", + "sourceChannelId": "e4e962a7-7d7c-4361-a271-fc6c52b19466", + "name": "C-SPAN HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/CSPAN.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/c2be2b4a-5f5f-43d9-acde-0110dec73251.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:38:22.183Z", + "sourceUpdatedAt": "2026-06-09T01:26:18.370Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:2e438a75-284a-4fe9-b64b-9fe3faf0765a", + "source": "dulo", + "sourceChannelId": "2e438a75-284a-4fe9-b64b-9fe3faf0765a", + "name": "Cartoon Network East HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/CartoonNetwork.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/0ca12fc0-73c4-4c1c-a259-82cd7d90f580.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:39:21.717Z", + "sourceUpdatedAt": "2026-05-31T02:31:08.839Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:5b2ceda1-36f9-4f47-aea9-fa8d04b3aa46", + "source": "dulo", + "sourceChannelId": "5b2ceda1-36f9-4f47-aea9-fa8d04b3aa46", + "name": "CMT HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/CMT.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/81cffdeb-5d41-4222-adb9-68f85d58094e.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:39:48.207Z", + "sourceUpdatedAt": "2026-06-09T05:38:52.839Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:a55f3e69-7941-4f12-9300-83a61f8f7339", + "source": "dulo", + "sourceChannelId": "a55f3e69-7941-4f12-9300-83a61f8f7339", + "name": "CNBC HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/CNBC.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/f9d027af-a9d2-4c59-865e-7101f7602966.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:40:33.795Z", + "sourceUpdatedAt": "2026-06-09T01:27:07.764Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:d435a43b-8fee-4a39-86c6-dc24f494bdcc", + "source": "dulo", + "sourceChannelId": "d435a43b-8fee-4a39-86c6-dc24f494bdcc", + "name": "CNN HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/CNN.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/bd76a4bc-4dec-4fc7-b200-e0b9f0302dbf.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:41:05.259Z", + "sourceUpdatedAt": "2026-06-09T01:27:22.208Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:3b00d176-d30d-4818-aeca-04f8bb3a8006", + "source": "dulo", + "sourceChannelId": "3b00d176-d30d-4818-aeca-04f8bb3a8006", + "name": "Comedy Central HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/ComedyCentral.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/e3d94c5a-a969-491a-9527-092b9ebb3424.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:42:46.336Z", + "sourceUpdatedAt": "2026-06-09T01:27:41.568Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:4e95c735-dda1-4717-87c1-7ffff63fddff", + "source": "dulo", + "sourceChannelId": "4e95c735-dda1-4717-87c1-7ffff63fddff", + "name": "Discovery Channel HD | USA", + "category": "documentary", + "groupKey": "documentary", + "groupLabel": "documentary", + "logoUrl": "https://iptvboss.xyz/logos/USA/DiscoveryChannel.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/cc4fe5ad-82d3-46cf-b3ec-6214772ce8c9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:43:28.368Z", + "sourceUpdatedAt": "2026-05-31T04:06:49.379Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:463e7954-6e71-4d25-b266-50fccc17a5bd", + "source": "dulo", + "sourceChannelId": "463e7954-6e71-4d25-b266-50fccc17a5bd", + "name": "Disney Channel HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/DisneyChannel.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/5ae3e3d6-e8de-4e6b-a1e4-44d08c52c1b9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:44:11.811Z", + "sourceUpdatedAt": "2026-05-31T03:57:02.924Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:3664b2cb-31dc-4274-944b-73136a47f26e", + "source": "dulo", + "sourceChannelId": "3664b2cb-31dc-4274-944b-73136a47f26e", + "name": "Disney Jr HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/DisneyJunior.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/edce9dc6-ea78-484d-a6ae-8fc695680c5d.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:44:44.857Z", + "sourceUpdatedAt": "2026-05-31T04:41:37.631Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:b6fbc14a-bcdf-4c4d-88e1-78a94aec05f2", + "source": "dulo", + "sourceChannelId": "b6fbc14a-bcdf-4c4d-88e1-78a94aec05f2", + "name": "E! HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/EEntertainmentTelevision.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/d72e883f-c569-4c7f-9e6e-7466fcb77df6.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:45:58.725Z", + "sourceUpdatedAt": "2026-06-09T01:28:23.944Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e84387d8-0d08-4766-93e0-8d3c39fb721c", + "source": "dulo", + "sourceChannelId": "e84387d8-0d08-4766-93e0-8d3c39fb721c", + "name": "epiX HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/Epix.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/1e79ae69-e06d-4ace-ae7b-c2d8769c2d46.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:46:34.951Z", + "sourceUpdatedAt": "2026-06-09T01:28:38.783Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:18037a7e-3daa-4ca5-8eb7-40ad0ba09776", + "source": "dulo", + "sourceChannelId": "18037a7e-3daa-4ca5-8eb7-40ad0ba09776", + "name": "ESPN 2 HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ESPN2.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/762c0cd1-e3d5-4cc6-8b5c-fe058f74634c.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:47:11.201Z", + "sourceUpdatedAt": "2026-06-09T01:29:15.715Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:ef9447e6-bee7-4de3-80bc-9eb215a925df", + "source": "dulo", + "sourceChannelId": "ef9447e6-bee7-4de3-80bc-9eb215a925df", + "name": "ESPN HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ESPN.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/22d6a3ff-655d-4a28-be98-b9705d80919b.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:48:55.100Z", + "sourceUpdatedAt": "2026-06-09T01:28:54.745Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:d80a1087-c4d1-4c0c-9f7d-1c639784ed7e", + "source": "dulo", + "sourceChannelId": "d80a1087-c4d1-4c0c-9f7d-1c639784ed7e", + "name": "ESPN News HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ESPNEWS.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/a6f4cb11-c02f-4b0b-8fab-ba5147800479.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:49:37.250Z", + "sourceUpdatedAt": "2026-06-09T01:29:32.777Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:1263c734-c158-4936-969b-f46e5d21bef4", + "source": "dulo", + "sourceChannelId": "1263c734-c158-4936-969b-f46e5d21bef4", + "name": "ESPN U HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/ESPNU.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/69bb2fd6-b379-4612-971c-c613e0868ef2.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:50:12.026Z", + "sourceUpdatedAt": "2026-06-09T01:29:45.906Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e51f0e58-0f0b-476e-b2a5-84033adcc2c2", + "source": "dulo", + "sourceChannelId": "e51f0e58-0f0b-476e-b2a5-84033adcc2c2", + "name": "Food Network HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoodNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/d5952b5e-8fd3-42f7-a6c9-468ea8cbec1d.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:51:00.312Z", + "sourceUpdatedAt": "2026-06-09T01:29:59.661Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:9e26f026-1202-4501-bb0a-faa9edd6e6a8", + "source": "dulo", + "sourceChannelId": "9e26f026-1202-4501-bb0a-faa9edd6e6a8", + "name": "Fox 5 NY HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoxEast_WNYW.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/f022aa37-6621-4c5a-b321-4a204c75743a.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:51:42.636Z", + "sourceUpdatedAt": "2026-06-09T01:30:12.769Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:b97baa93-8be4-4dd7-b7bf-29f24fe401f2", + "source": "dulo", + "sourceChannelId": "b97baa93-8be4-4dd7-b7bf-29f24fe401f2", + "name": "Fox Business HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoxBusiness.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/9509f30b-0b7a-4761-b18d-cb47644a414b.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:53:01.618Z", + "sourceUpdatedAt": "2026-06-09T01:30:26.379Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:509cda12-8077-4c82-a2aa-10ba512fcc75", + "source": "dulo", + "sourceChannelId": "509cda12-8077-4c82-a2aa-10ba512fcc75", + "name": "Fox News HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoxNewsChannel.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/f2134c41-965e-4ffa-9d0b-3a8fb0b576e7.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:53:51.758Z", + "sourceUpdatedAt": "2026-06-09T01:30:42.970Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:4567e88a-0e47-4e61-ab50-6d9ff92252e2", + "source": "dulo", + "sourceChannelId": "4567e88a-0e47-4e61-ab50-6d9ff92252e2", + "name": "Fox Sports 1 HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoxSports1.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/124a2e29-8677-48cc-b8be-2b00f748e497.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:55:22.886Z", + "sourceUpdatedAt": "2026-06-09T01:30:55.020Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:22483c87-bd9c-4880-8970-98a532680d40", + "source": "dulo", + "sourceChannelId": "22483c87-bd9c-4880-8970-98a532680d40", + "name": "Fox Sports 2 HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/FoxSports2.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/d1f8a09a-0c14-4445-a3a4-9b27982da2fb.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:55:59.774Z", + "sourceUpdatedAt": "2026-06-09T01:31:06.611Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:3118f2f8-b956-4214-9c58-b07e78a06b21", + "source": "dulo", + "sourceChannelId": "3118f2f8-b956-4214-9c58-b07e78a06b21", + "name": "FreeForm HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/Freeform.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/e7af2de6-2703-47dd-9af1-10b3048b1e9d.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:56:52.600Z", + "sourceUpdatedAt": "2026-06-09T01:31:21.566Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:08369fe5-f318-4a89-982d-9f63c537302e", + "source": "dulo", + "sourceChannelId": "08369fe5-f318-4a89-982d-9f63c537302e", + "name": "FX HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/FX.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/8a3432fb-04a7-4ade-a28f-5c1a0cc3f68e.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:58:31.867Z", + "sourceUpdatedAt": "2026-05-31T04:12:55.759Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:da593975-ed36-41eb-889d-defc075ac64a", + "source": "dulo", + "sourceChannelId": "da593975-ed36-41eb-889d-defc075ac64a", + "name": "Hallmark Family HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HallmarkFamily.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/2fab5943-6302-4d69-beaf-57c64e65a3d5.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T02:59:20.663Z", + "sourceUpdatedAt": "2026-05-31T04:14:47.223Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:19395309-c667-409d-9104-3196557a6c2a", + "source": "dulo", + "sourceChannelId": "19395309-c667-409d-9104-3196557a6c2a", + "name": "HBO 2 HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HBO2.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/af8aa9b5-ddaf-483f-bbbc-34bc24d180c1.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:02:20.451Z", + "sourceUpdatedAt": "2026-05-31T04:16:00.039Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:f9a9b988-868e-4014-8bcd-fe32da862d39", + "source": "dulo", + "sourceChannelId": "f9a9b988-868e-4014-8bcd-fe32da862d39", + "name": "HBO Family HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HBOFamily.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/4024db2b-e32f-49e2-936c-db299b7b3c70.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:06:01.005Z", + "sourceUpdatedAt": "2026-05-31T04:17:20.507Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:4499e99d-8e5b-4087-9d51-9c61a820ca02", + "source": "dulo", + "sourceChannelId": "4499e99d-8e5b-4087-9d51-9c61a820ca02", + "name": "HBO HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HBO.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/40b05b79-ee37-4d80-81be-fecb99539003.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:06:42.119Z", + "sourceUpdatedAt": "2026-06-01T01:21:54.617Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:fabb4415-d644-44dd-b083-dab8c77d8833", + "source": "dulo", + "sourceChannelId": "fabb4415-d644-44dd-b083-dab8c77d8833", + "name": "HBO Latino HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HBOLatino.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/1af71434-74c1-4e5d-a1f9-ec8b005c6bbd.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:07:23.376Z", + "sourceUpdatedAt": "2026-06-09T01:54:20.306Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:8cf4517b-a7c4-4e97-b2de-7ff3ada11543", + "source": "dulo", + "sourceChannelId": "8cf4517b-a7c4-4e97-b2de-7ff3ada11543", + "name": "HBO Signature HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/HBOSignature.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/51d76cab-aff4-435a-ae57-88c108092ba7.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:08:02.732Z", + "sourceUpdatedAt": "2026-05-31T04:21:39.827Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:72b7a1df-54ef-4ca1-9c82-e526977775c5", + "source": "dulo", + "sourceChannelId": "72b7a1df-54ef-4ca1-9c82-e526977775c5", + "name": "HGTV HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/HGTV.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/6ed802f7-51b2-459d-8f90-4198bc46f61f.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:09:02.609Z", + "sourceUpdatedAt": "2026-06-09T01:32:17.646Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:6f629954-45bb-4263-a58e-ba47f9277ef5", + "source": "dulo", + "sourceChannelId": "6f629954-45bb-4263-a58e-ba47f9277ef5", + "name": "History HD | USA", + "category": "documentary", + "groupKey": "documentary", + "groupLabel": "documentary", + "logoUrl": "https://iptvboss.xyz/logos/USA/History.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/732bd308-3c5e-4260-9626-9dee58f3990f.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:09:32.616Z", + "sourceUpdatedAt": "2026-06-09T01:32:35.637Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:50fea76c-3c5b-45b9-a8c2-31093fd6541b", + "source": "dulo", + "sourceChannelId": "50fea76c-3c5b-45b9-a8c2-31093fd6541b", + "name": "Investigation Discovery HD | USA", + "category": "documentary", + "groupKey": "documentary", + "groupLabel": "documentary", + "logoUrl": "https://iptvboss.xyz/logos/USA/InvestigationDiscovery.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/cc30205b-d5b7-47ca-bf66-784f4dbe9bdd.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:10:17.256Z", + "sourceUpdatedAt": "2026-05-31T04:09:42.485Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:dc23c106-df27-4364-9dcc-7d3a30ad44e2", + "source": "dulo", + "sourceChannelId": "dc23c106-df27-4364-9dcc-7d3a30ad44e2", + "name": "Lifetime HD | TV", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/Lifetime.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/c00b0f90-4a1b-4846-b676-c2eeff8a01bf.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:11:13.848Z", + "sourceUpdatedAt": "2026-06-09T01:39:07.679Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:d0bc8a90-1bd2-4e7f-8875-0b2be40888f8", + "source": "dulo", + "sourceChannelId": "d0bc8a90-1bd2-4e7f-8875-0b2be40888f8", + "name": "Magnolia Network HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/MagnoliaNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/0a16d94a-fda9-4d63-bb7a-c7003f7336c8.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:11:51.707Z", + "sourceUpdatedAt": "2026-06-09T01:39:24.083Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:88242062-2bfd-4547-9043-b2189399f43e", + "source": "dulo", + "sourceChannelId": "88242062-2bfd-4547-9043-b2189399f43e", + "name": "MLB Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/MLBNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/a185d75f-7635-4b51-bb42-e6d776917d14.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:18:32.884Z", + "sourceUpdatedAt": "2026-06-09T01:39:40.798Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:ae1bbb1c-fce3-4768-b13a-1417b27e0c76", + "source": "dulo", + "sourceChannelId": "ae1bbb1c-fce3-4768-b13a-1417b27e0c76", + "name": "MSG HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/MSG.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/84239837-d11a-43d7-a645-923e1554e7ba.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:19:11.991Z", + "sourceUpdatedAt": "2026-06-09T01:40:43.439Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:a3d24518-0ec0-4a60-bb74-43e62f923277", + "source": "dulo", + "sourceChannelId": "a3d24518-0ec0-4a60-bb74-43e62f923277", + "name": "MSNBC HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/MSNBC.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/e77e70b2-f513-4f5c-ba45-42743dbc5453.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:19:51.602Z", + "sourceUpdatedAt": "2026-06-09T01:40:59.649Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:96d5474d-facb-48f0-875e-6e1836ecf5c2", + "source": "dulo", + "sourceChannelId": "96d5474d-facb-48f0-875e-6e1836ecf5c2", + "name": "MTV HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/MTV.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/4484de6d-491b-4a3d-967f-08e68b63b174.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:20:42.824Z", + "sourceUpdatedAt": "2026-06-09T01:41:14.485Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:2798d6d7-b2e5-46f0-8753-7be9c3cfcf22", + "source": "dulo", + "sourceChannelId": "2798d6d7-b2e5-46f0-8753-7be9c3cfcf22", + "name": "National Geographic HD | USA", + "category": "documentary", + "groupKey": "documentary", + "groupLabel": "documentary", + "logoUrl": "https://iptvboss.xyz/logos/USA/NationalGeographic.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/fa02d70f-4214-4f94-ac71-d612b90468a6.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:22:45.607Z", + "sourceUpdatedAt": "2026-06-09T01:41:38.163Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:2d707c68-d53a-4174-9b3a-78e44b300e63", + "source": "dulo", + "sourceChannelId": "2d707c68-d53a-4174-9b3a-78e44b300e63", + "name": "NBA TV HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NBAtv.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/0d6106f5-50e4-43f1-8ab5-431dac6fc4c0.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:23:17.032Z", + "sourceUpdatedAt": "2026-06-09T01:42:00.396Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:671a4cce-efa5-4254-a2fc-d17278e3ded3", + "source": "dulo", + "sourceChannelId": "671a4cce-efa5-4254-a2fc-d17278e3ded3", + "name": "NESN HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NESN.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/8bd363bf-6fb2-4315-ac1d-1c1952f7f4cd.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:24:12.362Z", + "sourceUpdatedAt": "2026-06-09T01:44:35.991Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:312c8261-c482-4346-8437-83eb9436c91b", + "source": "dulo", + "sourceChannelId": "312c8261-c482-4346-8437-83eb9436c91b", + "name": "NFL Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NFLNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/15816015-c1d0-47da-81e9-ae68e9d2b67f.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:25:18.812Z", + "sourceUpdatedAt": "2026-06-09T01:44:48.153Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:1e89de9c-999b-4193-8414-a444631e3728", + "source": "dulo", + "sourceChannelId": "1e89de9c-999b-4193-8414-a444631e3728", + "name": "NHL Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NHLNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/4546b90f-28a7-4f12-88c3-856df043e237.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:25:52.205Z", + "sourceUpdatedAt": "2026-06-09T01:45:04.240Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:6b75e415-e3a5-4815-9cf2-237e7f11ae2e", + "source": "dulo", + "sourceChannelId": "6b75e415-e3a5-4815-9cf2-237e7f11ae2e", + "name": "Nickelodeon East HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/Nickelodeon.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/59c180f4-1bdc-488c-8e29-9e051f477392.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:26:51.363Z", + "sourceUpdatedAt": "2026-05-31T04:00:33.085Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:861952e3-3157-4635-9944-d2d074a817ab", + "source": "dulo", + "sourceChannelId": "861952e3-3157-4635-9944-d2d074a817ab", + "name": "Nicktoons HD | USA", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://iptvboss.xyz/logos/USA/Nicktoons.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/4108a8ad-602e-4a25-9f71-21cd1338a403.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:27:22.543Z", + "sourceUpdatedAt": "2026-05-31T04:02:04.205Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:27dfd93f-08da-4a65-8d9f-b174f7062d79", + "source": "dulo", + "sourceChannelId": "27dfd93f-08da-4a65-8d9f-b174f7062d79", + "name": "OWN HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/OWN.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/ca273200-b240-48cc-b03a-31da3e2e7d75.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:28:06.545Z", + "sourceUpdatedAt": "2026-06-09T01:47:32.812Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:be1f5f0e-80c8-4fb1-a2ff-fe6a6e55e2de", + "source": "dulo", + "sourceChannelId": "be1f5f0e-80c8-4fb1-a2ff-fe6a6e55e2de", + "name": "Paramount Network HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/ParamountNetwork.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/e8226677-87ba-4f1f-ba23-7b992c6336fb.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:28:38.876Z", + "sourceUpdatedAt": "2026-05-29T07:36:55.489Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:116bab23-7ece-4230-94d1-796631e244c5", + "source": "dulo", + "sourceChannelId": "116bab23-7ece-4230-94d1-796631e244c5", + "name": "Showtime Extreme HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/ShowtimeExtreme.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/ba77ea81-a931-48aa-964e-6536852f60cb.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:30:39.298Z", + "sourceUpdatedAt": "2026-05-31T04:24:31.087Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:62f075b0-cdd3-4fdb-b0e9-e32c49285836", + "source": "dulo", + "sourceChannelId": "62f075b0-cdd3-4fdb-b0e9-e32c49285836", + "name": "Sony Movie Channel HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/SonyMovieChannel.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/3d0c8718-92d6-4734-8448-863a9481b8d7.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:37:54.799Z", + "sourceUpdatedAt": "2026-06-09T02:04:09.488Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:b027a9ab-e147-405e-b77c-868f2f225300", + "source": "dulo", + "sourceChannelId": "b027a9ab-e147-405e-b77c-868f2f225300", + "name": "Spectrum SportsNet LA Dodgers HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/SpectrumSportsNetLADodgers.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/4a0c2a1f-bbb5-4cf0-998a-31b2eeb5869a.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:39:06.913Z", + "sourceUpdatedAt": "2026-06-09T16:23:36.864Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:5c83b496-df9e-4dae-9554-d93cd482177a", + "source": "dulo", + "sourceChannelId": "5c83b496-df9e-4dae-9554-d93cd482177a", + "name": "Starz Cinema HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/StarzCinema.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/2df70247-3213-47e3-8553-bc8c28d0c67f.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:40:15.481Z", + "sourceUpdatedAt": "2026-05-31T04:29:37.115Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:667683d0-e7d7-4f10-af12-5174ee74d8ed", + "source": "dulo", + "sourceChannelId": "667683d0-e7d7-4f10-af12-5174ee74d8ed", + "name": "Starz East HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/Starz.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/8789fb20-a653-465f-b491-937724e3765b.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:40:41.308Z", + "sourceUpdatedAt": "2026-05-31T04:30:59.041Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:126761fd-9c50-46d7-9821-a7503528fe7f", + "source": "dulo", + "sourceChannelId": "126761fd-9c50-46d7-9821-a7503528fe7f", + "name": "Starz Encore HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/StarzEncore.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/5e336b09-a46c-4a0f-b72f-886c30dc92cb.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:41:06.243Z", + "sourceUpdatedAt": "2026-05-31T04:32:13.702Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:11d8bbd6-b65b-4138-ac22-0194346db38e", + "source": "dulo", + "sourceChannelId": "11d8bbd6-b65b-4138-ac22-0194346db38e", + "name": "SyFy HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/Syfy.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/010d6d1d-a8ed-42dc-abc5-82124ede2a59.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:41:42.962Z", + "sourceUpdatedAt": "2026-05-31T04:34:35.485Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:b1428e38-1dd2-41ef-94b6-f2f24b435a63", + "source": "dulo", + "sourceChannelId": "b1428e38-1dd2-41ef-94b6-f2f24b435a63", + "name": "Tennis Channel HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/TennisChannel.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/632c60eb-7798-4695-b617-0271e22b47d1.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:42:21.397Z", + "sourceUpdatedAt": "2026-05-29T07:51:03.463Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:5d135d5f-23f5-4c77-881e-dd074ad1844e", + "source": "dulo", + "sourceChannelId": "5d135d5f-23f5-4c77-881e-dd074ad1844e", + "name": "The Weather Channel HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/TheWeatherChannel.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/faa13ade-5cc2-451a-86b2-b22de56da424.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:43:01.773Z", + "sourceUpdatedAt": "2026-05-29T07:53:17.451Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:ac80172e-e8ff-4a06-b956-bc25bf3b519f", + "source": "dulo", + "sourceChannelId": "ac80172e-e8ff-4a06-b956-bc25bf3b519f", + "name": "TNT HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/TNT.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/875059b4-6cfa-4be2-9e86-159be43583e8.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:43:36.092Z", + "sourceUpdatedAt": "2026-05-29T07:54:14.862Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:f822a7fa-ed38-4a95-aa8f-69ad98fcbce7", + "source": "dulo", + "sourceChannelId": "f822a7fa-ed38-4a95-aa8f-69ad98fcbce7", + "name": "TruTV HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/truTV.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/2965b183-a648-4866-b598-3e193785d33f.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:44:42.470Z", + "sourceUpdatedAt": "2026-05-29T07:55:23.124Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:74d709fa-01e7-4499-bc3e-d1a6b3540948", + "source": "dulo", + "sourceChannelId": "74d709fa-01e7-4499-bc3e-d1a6b3540948", + "name": "USA Network HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/USANetwork.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/744d72b2-0d9c-45c0-9824-6f030ad06ad3.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:45:16.676Z", + "sourceUpdatedAt": "2026-05-29T07:56:09.163Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:344d07b6-4b72-4aae-94ce-c1a508167d3d", + "source": "dulo", + "sourceChannelId": "344d07b6-4b72-4aae-94ce-c1a508167d3d", + "name": "VH1 HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/VH1.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/34ae4c6e-82c1-4ef4-b871-ac82bf960154.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:46:00.402Z", + "sourceUpdatedAt": "2026-05-29T07:57:09.238Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:c7e4a13a-142f-433c-8fe3-55cbae1e67d5", + "source": "dulo", + "sourceChannelId": "c7e4a13a-142f-433c-8fe3-55cbae1e67d5", + "name": "Willow Cricket HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/WillowCricket.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/7cdbfb8d-c614-4f92-870c-675d5d890d5e.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T03:46:35.984Z", + "sourceUpdatedAt": "2026-06-09T05:06:34.327Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:36c4d9d1-4d43-45f7-8477-c1bfbe0ad217", + "source": "dulo", + "sourceChannelId": "36c4d9d1-4d43-45f7-8477-c1bfbe0ad217", + "name": "Sportsnet East HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/SportsnetEast.ca.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/0c29406e-bf63-4393-8b07-cf594efdbc3c.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T04:27:41.947Z", + "sourceUpdatedAt": "2026-06-09T05:25:35.922Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:688ddade-4c5f-4b1a-9fe3-b3565475b5dd", + "source": "dulo", + "sourceChannelId": "688ddade-4c5f-4b1a-9fe3-b3565475b5dd", + "name": "TSN 1 HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/TSN1.ca.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/3929afa8-022d-4bda-af5c-aa39c33b05de.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T04:28:36.295Z", + "sourceUpdatedAt": "2026-05-29T08:04:59.296Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e7756305-b952-456a-8aa8-8253fcb77209", + "source": "dulo", + "sourceChannelId": "e7756305-b952-456a-8aa8-8253fcb77209", + "name": "TSN 2 HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/TSN2.ca.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/79e4cfd2-676b-48de-b035-54f5ef7dfa28.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T04:31:08.698Z", + "sourceUpdatedAt": "2026-05-29T08:06:06.135Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:cf99e9c5-9cb8-41a9-81fd-1e27e56ecead", + "source": "dulo", + "sourceChannelId": "cf99e9c5-9cb8-41a9-81fd-1e27e56ecead", + "name": "TSN 3 HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/TSN3.ca.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/8176f064-c2d0-4cba-9899-c533e80d68a8.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-21T04:31:59.186Z", + "sourceUpdatedAt": "2026-05-29T08:07:13.796Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:83fd2f2e-249f-43b4-b09b-dc9de11137f9", + "source": "dulo", + "sourceChannelId": "83fd2f2e-249f-43b4-b09b-dc9de11137f9", + "name": "GSN HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/GameShowNetworkPacific.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/d8f0da98-667f-4ff1-b090-79ec3df30a97.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-24T20:00:02.586Z", + "sourceUpdatedAt": "2026-06-09T01:31:57.958Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:c4dda23a-c202-4e2c-8be8-f5ed5e748534", + "source": "dulo", + "sourceChannelId": "c4dda23a-c202-4e2c-8be8-f5ed5e748534", + "name": "TNT Sports 1 HD | UK", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/UK/TNTSports1.uk.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/e773710d-d3e5-4e03-8470-3a6070cf9285.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-25T00:36:12.494Z", + "sourceUpdatedAt": "2026-05-29T08:09:26.192Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:7f37f6cb-d5c2-4003-87e2-780c942d1f8d", + "source": "dulo", + "sourceChannelId": "7f37f6cb-d5c2-4003-87e2-780c942d1f8d", + "name": "TNT Sports 2 HD | UK", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/UK/TNTSports2.uk.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/4297a668-7c2f-48dd-b6f3-d07d539c3b26.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-25T00:43:02.010Z", + "sourceUpdatedAt": "2026-05-29T08:10:01.685Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:e1d60af0-164b-41bf-abd4-1710aed0911b", + "source": "dulo", + "sourceChannelId": "e1d60af0-164b-41bf-abd4-1710aed0911b", + "name": "NBC Sports Bay Area HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NBCSportsBayArea.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/713cb02d-2bb3-4259-9bb1-f1b8174a16a1.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T17:43:41.261Z", + "sourceUpdatedAt": "2026-05-29T08:11:10.230Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:682e3578-7e54-4e67-b856-79052ee22d8e", + "source": "dulo", + "sourceChannelId": "682e3578-7e54-4e67-b856-79052ee22d8e", + "name": "NBC Sports California HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/NBCSportsCalifornia.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/153aceee-ff14-495c-8620-9cfe9e9e505b.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T17:50:35.097Z", + "sourceUpdatedAt": "2026-05-29T08:12:08.901Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:a4c38821-8716-4e9e-b755-9524fbae8886", + "source": "dulo", + "sourceChannelId": "a4c38821-8716-4e9e-b755-9524fbae8886", + "name": "Travel Channel HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/TravelChannel.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/6642f732-1ad7-4273-b30a-d12cd639f99c.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:19:46.814Z", + "sourceUpdatedAt": "2026-05-29T08:13:22.968Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:84522924-f79c-4cd4-bb22-df9f19502ce7", + "source": "dulo", + "sourceChannelId": "84522924-f79c-4cd4-bb22-df9f19502ce7", + "name": "Destination America HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/DestinationAmerica.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/685d35e8-8e90-4ceb-9241-a18a17056b34.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:22:32.872Z", + "sourceUpdatedAt": "2026-06-09T01:27:57.588Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:a06ea1dd-0304-4325-9f15-5f5e6969849f", + "source": "dulo", + "sourceChannelId": "a06ea1dd-0304-4325-9f15-5f5e6969849f", + "name": "fyi HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/FYIChannel.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/4fd9553b-efc3-4b15-8c81-c27d861fa94d.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:26:25.132Z", + "sourceUpdatedAt": "2026-06-09T01:31:39.413Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:df2cc909-cdda-4061-bebe-b31d210fa62c", + "source": "dulo", + "sourceChannelId": "df2cc909-cdda-4061-bebe-b31d210fa62c", + "name": "NBC 4 HD | US", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/NBCWNBC.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/fe609bb0-14f7-4e69-9c1d-66afa127d03d.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:31:17.645Z", + "sourceUpdatedAt": "2026-06-09T01:43:12.336Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:4f3ddbdb-38ea-4853-8fad-404f0899b1dd", + "source": "dulo", + "sourceChannelId": "4f3ddbdb-38ea-4853-8fad-404f0899b1dd", + "name": "CBS 2 NY HD | USA", + "category": "news", + "groupKey": "news", + "groupLabel": "news", + "logoUrl": "https://iptvboss.xyz/logos/USA/WCBS.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/bbf679fc-bf77-4ac4-a988-b36195bd1e31.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:33:45.721Z", + "sourceUpdatedAt": "2026-06-09T01:26:38.492Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:0e5da8bf-5203-4abf-8dd1-ed7e28b33f10", + "source": "dulo", + "sourceChannelId": "0e5da8bf-5203-4abf-8dd1-ed7e28b33f10", + "name": "Smithsonian Channel HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/smithsonianchannel.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/3c03d782-4a10-43e4-8209-b23ee2759e88.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-03-26T18:40:30.072Z", + "sourceUpdatedAt": "2026-05-29T08:18:37.144Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:5e675be4-89a2-43d7-8917-3c6e78acc30b", + "source": "dulo", + "sourceChannelId": "5e675be4-89a2-43d7-8917-3c6e78acc30b", + "name": "TSN 4 HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/TSN4.ca.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/7c780698-133a-48ef-bdbf-585c4b7ab6c9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-04-07T21:21:50.538Z", + "sourceUpdatedAt": "2026-05-29T08:19:43.457Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:aaf601ce-6337-4f14-ac22-e2118f49072c", + "source": "dulo", + "sourceChannelId": "aaf601ce-6337-4f14-ac22-e2118f49072c", + "name": "TSN 5 HD | CA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/Canada/TSN5.ca.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/48a842fe-0f1d-4daf-ad63-c930ac09d1bf.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-04-07T21:23:40.840Z", + "sourceUpdatedAt": "2026-05-29T08:20:29.565Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:98a9cd29-0a98-4cb1-8334-18067b9d47ac", + "source": "dulo", + "sourceChannelId": "98a9cd29-0a98-4cb1-8334-18067b9d47ac", + "name": "CBS Sports HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/USA/CBSSportsNetwork.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/46ea23e8-f030-41d4-a5f9-19cb3c5adc92.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-04-07T21:50:54.367Z", + "sourceUpdatedAt": "2026-06-09T01:26:49.254Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:94140129-0daf-4343-ab07-50fee471c779", + "source": "dulo", + "sourceChannelId": "94140129-0daf-4343-ab07-50fee471c779", + "name": "TBS HD | USA", + "category": "entertainment", + "groupKey": "entertainment", + "groupLabel": "entertainment", + "logoUrl": "https://iptvboss.xyz/logos/USA/TBS.us.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/cffea07a-4d5c-4764-8361-126aa6d28c84.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-02T19:41:54.342Z", + "sourceUpdatedAt": "2026-05-29T08:22:54.580Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:9050e056-6c10-4f86-9a75-a65fe8598e01", + "source": "dulo", + "sourceChannelId": "9050e056-6c10-4f86-9a75-a65fe8598e01", + "name": "Star Sports 1 FHD | IND", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/India/StarSports1.in.png", + "streamEntryUrl": "https://images.dulo.tv/memfs/eca1b43d-cbc1-4f71-b26d-c4add1decc54.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-06T13:27:55.894Z", + "sourceUpdatedAt": "2026-05-29T08:39:40.195Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:57e316ac-f55a-4d98-93da-5ca698875913", + "source": "dulo", + "sourceChannelId": "57e316ac-f55a-4d98-93da-5ca698875913", + "name": "TNT Sports 1 HD Backup | UK", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://iptvboss.xyz/logos/UK/TNTSports1.uk.png", + "streamEntryUrl": "https://cdn.dulo.tv/memfs/c015ed02-bdbd-471d-946a-c56646b47c62.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-06T20:37:20.144Z", + "sourceUpdatedAt": "2026-05-22T21:28:59.374Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:8c60ce75-1f0d-41e7-997e-1e71444871f4", + "source": "dulo", + "sourceChannelId": "8c60ce75-1f0d-41e7-997e-1e71444871f4", + "name": "Starz Encore Espanol HD | Latino", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://cdn.iptvboss.pro/logos/USA/StarzEncoreEspanol.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/5d4352d8-0e6e-465c-b65d-46adaaa69885.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T02:08:19.805Z", + "sourceUpdatedAt": "2026-05-31T04:35:58.163Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:95f88d4a-7349-4aec-b62c-06baaea70460", + "source": "dulo", + "sourceChannelId": "95f88d4a-7349-4aec-b62c-06baaea70460", + "name": "ESPN Deportes HD | Latino", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://cdn.iptvboss.pro/logos/USA/ESPNDeportes.us.png", + "streamEntryUrl": "https://bridge.dulo.tv/memfs/cd89307e-34c7-4efc-9e23-c6f1f60422ee.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T02:21:40.559Z", + "sourceUpdatedAt": "2026-05-26T01:47:38.901Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:2af62785-0139-499a-8f4d-ac35b702ef06", + "source": "dulo", + "sourceChannelId": "2af62785-0139-499a-8f4d-ac35b702ef06", + "name": "24/7 Peppa Pig", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://logos-world.net/wp-content/uploads/2024/01/Peppa-Pig-Logo-500x281.png", + "streamEntryUrl": "https://bridge.dulo.tv/memfs/754b034f-6353-46a9-842e-4f717ec5d194.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T02:41:58.640Z", + "sourceUpdatedAt": "2026-05-26T01:46:26.528Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:81a1ebb1-ba3e-4b13-a2a0-b6027575a815", + "source": "dulo", + "sourceChannelId": "81a1ebb1-ba3e-4b13-a2a0-b6027575a815", + "name": "24/7 SpongeBob SquarePants", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://1000logos.net/wp-content/uploads/2020/09/SpongeBob_SquarePants_logo.png", + "streamEntryUrl": "https://bridge.dulo.tv/memfs/a2e386d8-c6f0-4e9e-ba66-b232195f45f9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T02:43:59.742Z", + "sourceUpdatedAt": "2026-05-26T01:43:54.946Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:8c6bb9f3-548f-4e98-9ced-56178bb0beb3", + "source": "dulo", + "sourceChannelId": "8c6bb9f3-548f-4e98-9ced-56178bb0beb3", + "name": "24/7 Bluey", + "category": "kids", + "groupKey": "kids", + "groupLabel": "kids", + "logoUrl": "https://media.printables.com/media/prints/d35fce99-be65-436d-ae66-a833cc1a0754/images/10303645_a3372cce-e5aa-4679-9d42-52c1387e273c_0037f94f-e35b-498c-8100-1698b420d1fa/thumbs/cover/1200x630/png/bluey-logo.png", + "streamEntryUrl": "https://bridge.dulo.tv/memfs/79bcd103-4636-4a23-9372-f2a58fa49288.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T02:45:51.398Z", + "sourceUpdatedAt": "2026-05-26T01:45:14.956Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:c5b8ec5e-bec1-4632-9dff-8bf1cb33b655", + "source": "dulo", + "sourceChannelId": "c5b8ec5e-bec1-4632-9dff-8bf1cb33b655", + "name": "Cinemax HD | USA", + "category": "movies", + "groupKey": "movies", + "groupLabel": "movies", + "logoUrl": "https://iptvboss.xyz/logos/USA/CineMAX.us.png", + "streamEntryUrl": "https://hey.dulo.tv/memfs/b0bfb3af-3b3a-49bc-ace2-a0a1bdfb38e9.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-20T16:32:20.354Z", + "sourceUpdatedAt": "2026-05-31T04:37:54.058Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:8c4e39a2-745c-4bc5-a2da-a65cf91f8214", + "source": "dulo", + "sourceChannelId": "8c4e39a2-745c-4bc5-a2da-a65cf91f8214", + "name": "Chicago Sports Network HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://cdn.iptvboss.pro/logos/USA/ChicagoSportsNetwork.us.png", + "streamEntryUrl": "https://bridge.dulo.tv/memfs/a45a9917-b8e0-4473-bb0e-1ec2377be9d7.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-05-29T23:43:02.847Z", + "sourceUpdatedAt": "2026-05-29T23:43:42.123Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + }, + { + "_id": "dulo:44be3457-f36e-4cac-a696-c536cc333b0f", + "source": "dulo", + "sourceChannelId": "44be3457-f36e-4cac-a696-c536cc333b0f", + "name": "Golf Channel HD | USA", + "category": "sports", + "groupKey": "sports", + "groupLabel": "sports", + "logoUrl": "https://cdn.iptvboss.pro/logos/USA/GolfChannel.us.png", + "streamEntryUrl": "https://gotcha.dulo.tv/memfs/f10f14c7-c851-463c-839c-b86c643d33e0.m3u8", + "isPlayable": true, + "sourceCreatedAt": "2026-06-09T15:50:56.045Z", + "sourceUpdatedAt": "2026-06-09T15:50:56.045Z", + "ingestedAt": "2026-06-10T12:27:12.878Z" + } +] diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 00000000..69cfff8f --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,55 @@ +import { readFileSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +export interface AppConfig { + mongoUri: string; + port: number; + logLevel: string; +} + +const VALID_SCHEMES = /^mongodb(\+srv)?:\/\//; + +function resolveConfigPath(): string { + 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(): AppConfig { + const path = resolveConfigPath(); + const raw = readFileSync(path, 'utf8'); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Config file at "${path}" is not valid JSON: ${(err as Error).message}`); + } + + if (!parsed || typeof parsed !== 'object') { + throw new Error(`Config file at "${path}" must be a JSON object.`); + } + const obj = parsed as Record; + + 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 }; +} diff --git a/server/src/db.ts b/server/src/db.ts new file mode 100644 index 00000000..f79a4fda --- /dev/null +++ b/server/src/db.ts @@ -0,0 +1,27 @@ +import mongoose from 'mongoose'; + +export async function connect(uri: string): Promise { + 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(): Promise { + await mongoose.disconnect(); +} + +export function isConnected(): boolean { + return mongoose.connection.readyState === 1; +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 00000000..8d5cb218 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,86 @@ +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 as Error).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 as Error).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: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + 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: string) => { + 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); +}); diff --git a/server/src/models/ActiveStream.ts b/server/src/models/ActiveStream.ts new file mode 100644 index 00000000..693d184b --- /dev/null +++ b/server/src/models/ActiveStream.ts @@ -0,0 +1,29 @@ +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); diff --git a/server/src/models/Activity.ts b/server/src/models/Activity.ts new file mode 100644 index 00000000..832503d7 --- /dev/null +++ b/server/src/models/Activity.ts @@ -0,0 +1,13 @@ +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); diff --git a/server/src/models/Channel.ts b/server/src/models/Channel.ts new file mode 100644 index 00000000..31643597 --- /dev/null +++ b/server/src/models/Channel.ts @@ -0,0 +1,22 @@ +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); diff --git a/server/src/models/CustomPlaylist.ts b/server/src/models/CustomPlaylist.ts new file mode 100644 index 00000000..c969bf43 --- /dev/null +++ b/server/src/models/CustomPlaylist.ts @@ -0,0 +1,14 @@ +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); diff --git a/server/src/models/EpgSource.ts b/server/src/models/EpgSource.ts new file mode 100644 index 00000000..8422e131 --- /dev/null +++ b/server/src/models/EpgSource.ts @@ -0,0 +1,19 @@ +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); diff --git a/server/src/models/Playlist.ts b/server/src/models/Playlist.ts new file mode 100644 index 00000000..1ea812e0 --- /dev/null +++ b/server/src/models/Playlist.ts @@ -0,0 +1,22 @@ +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); diff --git a/server/src/models/PlaylistChannel.ts b/server/src/models/PlaylistChannel.ts new file mode 100644 index 00000000..27f4f54a --- /dev/null +++ b/server/src/models/PlaylistChannel.ts @@ -0,0 +1,15 @@ +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); diff --git a/server/src/models/Program.ts b/server/src/models/Program.ts new file mode 100644 index 00000000..51822ad0 --- /dev/null +++ b/server/src/models/Program.ts @@ -0,0 +1,16 @@ +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); diff --git a/server/src/models/SourceChannel.ts b/server/src/models/SourceChannel.ts new file mode 100644 index 00000000..53dd5372 --- /dev/null +++ b/server/src/models/SourceChannel.ts @@ -0,0 +1,49 @@ +import { Schema, model } from 'mongoose'; + +// SourceChannel — the canonical normalized channel document, adopted verbatim from d-combine's +// shared `channels` collection (see ../d-combine/docs/combine-architecture.md). One doc per channel +// across every (Default) source playlist, with a deterministic string `_id` (":") so +// re-imports/syncs upsert idempotently. The Vue UI never reads this shape directly — the sources +// router projects it through translate.ts (toUiChannel) into the legacy Channel shape on read. + +export interface SourceChannelDoc { + _id: string; // ":" — deterministic, collision-proof + source: string; // discriminator / UI heading / proxy prefix + sourceChannelId: string; // original upstream id as string (UUID, "51", SHA1…) + name: string; + category: string | null; // dulo: semantic; dlhd: null + groupKey: string; // UI bucket key (dulo: category; dlhd: first letter) + groupLabel: string; + logoUrl: string | null; // dulo/common: logo; dlhd: null + streamEntryUrl: string; // URL handed to the proxy (master .m3u8 or dlhd watch.php entry) + isPlayable: boolean; // false for malformed source URLs + sourceCreatedAt: string | null; // dulo timestamps; dlhd/common null + sourceUpdatedAt: string | null; + ingestedAt: string; // when buildSource/seed wrote it +} + +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); diff --git a/server/src/models/StreamSession.ts b/server/src/models/StreamSession.ts new file mode 100644 index 00000000..908d2cb4 --- /dev/null +++ b/server/src/models/StreamSession.ts @@ -0,0 +1,15 @@ +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); diff --git a/server/src/routes/activeStreams.ts b/server/src/routes/activeStreams.ts new file mode 100644 index 00000000..dccd9ddd --- /dev/null +++ b/server/src/routes/activeStreams.ts @@ -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); + } +}); diff --git a/server/src/routes/activity.ts b/server/src/routes/activity.ts new file mode 100644 index 00000000..1adadb21 --- /dev/null +++ b/server/src/routes/activity.ts @@ -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); + } +}); diff --git a/server/src/routes/channels.ts b/server/src/routes/channels.ts new file mode 100644 index 00000000..e5b8fc34 --- /dev/null +++ b/server/src/routes/channels.ts @@ -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= → 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); + } +}); diff --git a/server/src/routes/customPlaylists.ts b/server/src/routes/customPlaylists.ts new file mode 100644 index 00000000..12d39418 --- /dev/null +++ b/server/src/routes/customPlaylists.ts @@ -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); + } +}); diff --git a/server/src/routes/epgSources.ts b/server/src/routes/epgSources.ts new file mode 100644 index 00000000..c9c9faae --- /dev/null +++ b/server/src/routes/epgSources.ts @@ -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); + } +}); diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts new file mode 100644 index 00000000..2aa3af1e --- /dev/null +++ b/server/src/routes/health.ts @@ -0,0 +1,8 @@ +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' }); +}); diff --git a/server/src/routes/playlists.ts b/server/src/routes/playlists.ts new file mode 100644 index 00000000..19cf0965 --- /dev/null +++ b/server/src/routes/playlists.ts @@ -0,0 +1,113 @@ +import { Router } from 'express'; +import { Playlist } from '../models/Playlist.js'; +import { PlaylistChannel } from '../models/PlaylistChannel.js'; +import { Channel } from '../models/Channel.js'; +import { SourceChannel, type SourceChannelDoc } 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: { id: string; source?: string | null }): Promise { + 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<{ _id: string; count: number }>([ + { $group: { _id: '$playlistId', count: { $sum: 1 } } }, + ]), + SourceChannel.aggregate<{ _id: string; count: number }>([ + { $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); + } +}); diff --git a/server/src/routes/programs.ts b/server/src/routes/programs.ts new file mode 100644 index 00000000..af3549d7 --- /dev/null +++ b/server/src/routes/programs.ts @@ -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: Record> = {}; + 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); + } +}); diff --git a/server/src/routes/sources.ts b/server/src/routes/sources.ts new file mode 100644 index 00000000..1ebee75b --- /dev/null +++ b/server/src/routes/sources.ts @@ -0,0 +1,91 @@ +// 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, type RequestHandler } from 'express'; +import { SOURCES, getSource } from '../sources/registry.js'; +import { createProxyHandler } from '../sources/core/proxyHandler.js'; +import { createMetrics, snapshotOne, type Metrics } 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) as RequestHandler); +} + +// ── 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); +}); diff --git a/server/src/routes/streamSessions.ts b/server/src/routes/streamSessions.ts new file mode 100644 index 00000000..c00986d3 --- /dev/null +++ b/server/src/routes/streamSessions.ts @@ -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); + } +}); diff --git a/server/src/seed.ts b/server/src/seed.ts new file mode 100644 index 00000000..458f6ab1 --- /dev/null +++ b/server/src/seed.ts @@ -0,0 +1,176 @@ +// 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: 'IPTV-Pro Main synced — 142 channels, no changes' }, + { when: '12m', icon: 'epg', html: 'XMLTV UK Guide imported — 8,420 programs across 124 channels' }, + { when: '1h', icon: 'map', html: 'Manual mapping: HGTVhgtv.uk' }, + { when: '1h', icon: 'warn', html: 'Free UK Bouquet reports 3 channels offline (HTTP 503)' }, + { when: '3h', icon: 'edit', html: 'Renamed DiscoveryDiscovery Channel' }, + { when: 'Yest.', icon: 'add', html: 'Playlist IPTV-Pro Main 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: [string, string][] = [ + ['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: number) { + let s = seed; + return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; +} + +function generatePrograms(channelId: string, seedBase: number) { + const rng = rngFor(seedBase); + const progs: Array<{ channelId: string; start: number; end: number; title: string; cat: string }> = []; + 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); +}); diff --git a/server/src/sources/adapters/dulo.ts b/server/src/sources/adapters/dulo.ts new file mode 100644 index 00000000..9039aa52 --- /dev/null +++ b/server/src/sources/adapters/dulo.ts @@ -0,0 +1,118 @@ +// 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'; +import type { SourceAdapter } from '../types.js'; +import type { SourceChannelDoc } from '../../models/SourceChannel.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: unknown): boolean { + 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: unknown): string | null { + if (!ts || typeof ts !== 'string') return null; + const d = new Date(ts); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); +} + +const duloAdapter: SourceAdapter = { + 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()) as { channels?: any[] }; + 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')) as { channels?: any[] }; + return { + raw: snap.channels || [], + meta: { + endpoint: DULO_API, + live: false, + fallback: 'dulo.snapshot.json', + reason: (err as Error).message, + fetchedAt: new Date().toISOString(), + }, + }; + } + }, + + normalize(raw: any, { ingestedAt }): SourceChannelDoc | null { + 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: string) { + return { masterUrl: entryUrl }; // identity no-op + }, + + proxy: { + upstreamHeaders() { + return { Origin: DULO_ORIGIN }; // the memfs Origin allowlist gate + }, + isAllowedUpstream(url: string) { + 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: string, contentType: string) { + return contentType || 'application/octet-stream'; // plain TS — pass the upstream type through + }, + classifyArtifact(url: string) { + 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; diff --git a/server/src/sources/core/buildSource.ts b/server/src/sources/core/buildSource.ts new file mode 100644 index 00000000..d9fd3cb4 --- /dev/null +++ b/server/src/sources/core/buildSource.ts @@ -0,0 +1,54 @@ +// 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'; +import type { SourceAdapter } from '../types.js'; +import type { SourceChannelDoc } from '../../models/SourceChannel.js'; + +export interface BuildResult { + id: string; + count: number; + docs: SourceChannelDoc[]; + live: boolean; + meta: Record; +} + +export async function buildSource(adapter: SourceAdapter): Promise { + 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: SourceChannelDoc[] = []; + 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 as Error).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 }, + }; +} diff --git a/server/src/sources/core/logger.ts b/server/src/sources/core/logger.ts new file mode 100644 index 00000000..d6b437b0 --- /dev/null +++ b/server/src/sources/core/logger.ts @@ -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. + +type Level = 'info' | 'warn' | 'error' | 'ok'; + +function emit(level: Level, tag: string, msg: string): void { + 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: string, msg: string) => emit('info', tag, msg), + warn: (tag: string, msg: string) => emit('warn', tag, msg), + error: (tag: string, msg: string) => emit('error', tag, msg), + ok: (tag: string, msg: string) => emit('ok', tag, msg), +}; diff --git a/server/src/sources/core/metrics.ts b/server/src/sources/core/metrics.ts new file mode 100644 index 00000000..6c164e15 --- /dev/null +++ b/server/src/sources/core/metrics.ts @@ -0,0 +1,52 @@ +// 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 interface Metrics { + startedAt: number; + requests: { + total: number; + master: number; + variant: number; + segment: number; + other: number; + errors: number; + }; + upstream: { ok: number; notLive: number; forbidden: number; failed: number }; + bytesStreamed: number; + active: number; // proxy requests currently in flight + lastStreamAt: number | null; + lastError: string | null; +} + +export function createMetrics(): Metrics { + 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: number): string { + 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: Metrics) { + 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, + }; +} diff --git a/server/src/sources/core/playlist.ts b/server/src/sources/core/playlist.ts new file mode 100644 index 00000000..e71002ba --- /dev/null +++ b/server/src/sources/core/playlist.ts @@ -0,0 +1,45 @@ +// 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: string, contentType: string): boolean { + 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: string, + baseUrl: string, + prefix: string, + onChildHost: ((host: string) => void) | null, +): string { + 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'); +} diff --git a/server/src/sources/core/proxyHandler.ts b/server/src/sources/core/proxyHandler.ts new file mode 100644 index 00000000..e46eaf79 --- /dev/null +++ b/server/src/sources/core/proxyHandler.ts @@ -0,0 +1,162 @@ +// 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// +// · entry URL (dlhd watch.php / stream-N.php) → adapter.resolveStream() → fresh master, then proxy +// · master/variant .m3u8 → rewrite child URIs back through /api/v1//… +// · segment → pipe bytes (adapter may relabel the content-type) + +import { Readable } from 'node:stream'; +import type { Request, Response } from 'express'; +import { logger } from './logger.js'; +import { fmtBytes, type Metrics } from './metrics.js'; +import { looksLikePlaylist, rewritePlaylist } from './playlist.js'; +import type { SourceAdapter } from '../types.js'; + +function label(url: string): { host: string; short: string } { + 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: SourceAdapter, metrics: 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: Request, res: Response): Promise { + 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: string; + 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 as Error).message; + logger.warn(tag, `resolve failed: ${(err as Error).message} (${ms()})`); + res.status(502).type('text/plain').send(`Resolve failed: ${(err as Error).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: Awaited>; + try { + upstream = await fetch(upstreamUrl, { headers: proxy.upstreamHeaders(upstreamUrl) }); + } catch (err) { + metrics.upstream.failed++; + metrics.requests.errors++; + metrics.lastError = (err as Error).message; + logger.error(tag, `${type} ${host} ${short} upstream fetch failed: ${(err as Error).message} (${ms()})`); + res.status(502).type('text/plain').send(`Upstream fetch failed: ${(err as Error).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 as Parameters[0]); + body.on('data', (chunk: Buffer) => { + 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: Error) => { + metrics.upstream.failed++; + metrics.lastError = err.message; + logger.error(tag, `${type} ${host} ${short} stream error: ${err.message}`); + res.destroy(err); + }); + body.pipe(res); + }; +} diff --git a/server/src/sources/paths.ts b/server/src/sources/paths.ts new file mode 100644 index 00000000..9806ac18 --- /dev/null +++ b/server/src/sources/paths.ts @@ -0,0 +1,19 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// This module lives at /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 .source.json baselines + .snapshot.json fallbacks. */ +export const SEED_SOURCES_DIR = resolve(here, '..', '..', 'seed-data', 'sources'); + +export function bundleFile(sourceId: string): string { + return resolve(SEED_SOURCES_DIR, `${sourceId}.source.json`); +} + +export function snapshotFile(sourceId: string): string { + return resolve(SEED_SOURCES_DIR, `${sourceId}.snapshot.json`); +} diff --git a/server/src/sources/registry.ts b/server/src/sources/registry.ts new file mode 100644 index 00000000..d443f7d7 --- /dev/null +++ b/server/src/sources/registry.ts @@ -0,0 +1,13 @@ +// 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'; +import type { SourceAdapter } from './types.js'; + +export const SOURCES: SourceAdapter[] = [duloAdapter]; + +export function getSource(id: string): SourceAdapter | undefined { + return SOURCES.find((s) => s.id === id); +} diff --git a/server/src/sources/seed.ts b/server/src/sources/seed.ts new file mode 100644 index 00000000..4466435f --- /dev/null +++ b/server/src/sources/seed.ts @@ -0,0 +1,161 @@ +// Seed / init / sync / reset for the established (Default) source playlists. +// +// "Both: bundle + live sync" — the committed .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, type SourceChannelDoc } 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'; +import type { SourceAdapter } from './types.js'; + +export interface IntegrityReport { + id: string; + playlistExists: boolean; + channelCount: number; + ok: boolean; + issues: string[]; +} + +function readBundle(id: string): SourceChannelDoc[] { + 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 as SourceChannelDoc[]; +} + +function groupCount(docs: SourceChannelDoc[]): number { + 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: SourceChannelDoc[]): Promise { + 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 as any[], { ordered: false }); +} + +async function upsertPlaylistRow( + adapter: SourceAdapter, + groups: number, + opts: { lastSync: string; status: string }, +): Promise { + 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: string): Promise { + const issues: string[] = []; + 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'] as const) { + 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: string): Promise { + 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: string): Promise { + 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: string, +): Promise<{ report: IntegrityReport; live: boolean; count: number }> { + 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: string): Promise { + 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: { liveSync?: boolean } = {}): Promise { + 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 as Error).message}`, + ), + ); + } + } catch (err) { + logger.error('seed', `[${adapter.id}] init failed: ${(err as Error).message}`); + } + } +} diff --git a/server/src/sources/translate.ts b/server/src/sources/translate.ts new file mode 100644 index 00000000..63138a78 --- /dev/null +++ b/server/src/sources/translate.ts @@ -0,0 +1,69 @@ +// 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. + +import type { SourceChannelDoc } from '../models/SourceChannel.js'; + +export interface UiChannel { + id: string; // ":" + tvg_name: string; + group: string; + channel: number | null; // no source channel number + tvg_id: string | null; + state: 'active' | 'disabled'; + epg: 'matched' | 'unmatched' | null; // no EPG matching yet + status: string | null; // unknown until a stream is probed + res: string | null; // unknown until a stream is probed + source: string; + url: string; // streamEntryUrl (legacy field name) + logoColor: string; // derived deterministic fallback + initials: string; // derived from name + logoUrl: string | null; // real logo when the source provides one (dlhd: null) + streamEntryUrl: string; // explicit, for the player / proxyPath derivation + isPlayable: boolean; +} + +// Deterministic hue from a stable string → keeps a channel's fallback logo color stable across syncs. +function hueFromString(s: string): number { + let h = 0; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; + return h % 360; +} + +function logoColorFor(id: string): string { + return `oklch(0.5 0.16 ${hueFromString(id)})`; +} + +function initialsFor(name: string): string { + const ini = name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((w) => w[0]) + .join('') + .toUpperCase(); + return ini || '?'; +} + +export function toUiChannel(doc: SourceChannelDoc): UiChannel { + 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, + }; +} diff --git a/server/src/sources/types.ts b/server/src/sources/types.ts new file mode 100644 index 00000000..75cc28fd --- /dev/null +++ b/server/src/sources/types.ts @@ -0,0 +1,55 @@ +// The source-adapter contract, ported from d-combine (sources//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. + +import type { SourceChannelDoc } from '../models/SourceChannel.js'; + +export interface SourceMeta { + live?: boolean; + [k: string]: unknown; +} + +export interface RawListing { + // upstream-shaped records (JSON API rows, scraped cards, …) — the adapter boundary is untyped. + raw: any[]; + meta?: SourceMeta; +} + +export type ArtifactType = 'master' | 'variant' | 'segment' | 'other'; + +export interface SourceGrouping { + by: string; + groupOrder: string; + channelOrder: string; +} + +export interface SourceProxy { + /** Headers to inject on every upstream hop (dulo: Origin; dlhd: Referer+UA). */ + upstreamHeaders(url: string): Record; + /** SSRF gate for direct hops (dulo: *.dulo.tv; dlhd: dynamic Set; common: block private IPs). */ + isAllowedUpstream(url: string): boolean; + /** Per-rewritten-child hook (dlhd: dynamic-allow each host; dulo/common: null). */ + onPlaylistChildHost: ((host: string) => void) | null; + /** dulo/common: pass-through; dlhd: relabel disguised image/pdf TS as video/mp2t. */ + relabelSegmentContentType(url: string, contentType: string, type?: ArtifactType): string; + classifyArtifact(url: string): ArtifactType; +} + +export interface SourceAdapter { + id: string; + label: string; + /** Fetch/scrape raw listings → { raw, meta }; falls back to a bundled snapshot when offline. */ + listChannels(): Promise; + /** Map one raw record → one normalized document, or null to drop it. */ + normalize(raw: any, ctx: { ingestedAt: string }): SourceChannelDoc | null; + /** Serializable UI descriptor read by the SPA over /api/sources. */ + grouping: SourceGrouping; + /** Optional runtime provenance (dlhd: active mirror + probes). Absent → manifest statusUrl null. */ + status?: () => unknown | Promise; + /** Does this URL need server-side resolution before proxying? (dulo/common: false; dlhd: watch.php) */ + isEntryUrl(url: string): boolean; + /** Entry URL → { masterUrl }. dulo/common: identity; dlhd: 3-hop scrape. */ + resolveStream(entryUrl: string): Promise<{ masterUrl: string }>; + proxy: SourceProxy; +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..11e67d73 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*"] +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 00000000..9d65fffd --- /dev/null +++ b/src/App.vue @@ -0,0 +1,207 @@ + + + diff --git a/src/components/AddSourceModal.vue b/src/components/AddSourceModal.vue new file mode 100644 index 00000000..428f5c04 --- /dev/null +++ b/src/components/AddSourceModal.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/components/Btn.vue b/src/components/Btn.vue new file mode 100644 index 00000000..384f16d0 --- /dev/null +++ b/src/components/Btn.vue @@ -0,0 +1,25 @@ + + diff --git a/src/components/ChannelBulkDrawer.vue b/src/components/ChannelBulkDrawer.vue new file mode 100644 index 00000000..67525a2d --- /dev/null +++ b/src/components/ChannelBulkDrawer.vue @@ -0,0 +1,139 @@ + + + diff --git a/src/components/ChannelDrawer.vue b/src/components/ChannelDrawer.vue new file mode 100644 index 00000000..ac4ec96f --- /dev/null +++ b/src/components/ChannelDrawer.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/components/ChannelLogo.vue b/src/components/ChannelLogo.vue new file mode 100644 index 00000000..f23b4b0c --- /dev/null +++ b/src/components/ChannelLogo.vue @@ -0,0 +1,27 @@ + + diff --git a/src/components/Checkbox.vue b/src/components/Checkbox.vue new file mode 100644 index 00000000..b7fab7ed --- /dev/null +++ b/src/components/Checkbox.vue @@ -0,0 +1,8 @@ + + diff --git a/src/components/EndpointField.vue b/src/components/EndpointField.vue new file mode 100644 index 00000000..a82cae24 --- /dev/null +++ b/src/components/EndpointField.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/components/HlsPlayer.vue b/src/components/HlsPlayer.vue new file mode 100644 index 00000000..84e79922 --- /dev/null +++ b/src/components/HlsPlayer.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/Icon.vue b/src/components/Icon.vue new file mode 100644 index 00000000..537fd099 --- /dev/null +++ b/src/components/Icon.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/components/LogsDrawer.vue b/src/components/LogsDrawer.vue new file mode 100644 index 00000000..a6f8b1bd --- /dev/null +++ b/src/components/LogsDrawer.vue @@ -0,0 +1,195 @@ + + + diff --git a/src/components/Pill.vue b/src/components/Pill.vue new file mode 100644 index 00000000..85b2cac8 --- /dev/null +++ b/src/components/Pill.vue @@ -0,0 +1,6 @@ + + diff --git a/src/components/PlaylistStatusDrawer.vue b/src/components/PlaylistStatusDrawer.vue new file mode 100644 index 00000000..a4988bd8 --- /dev/null +++ b/src/components/PlaylistStatusDrawer.vue @@ -0,0 +1,151 @@ + + + diff --git a/src/components/SearchInput.vue b/src/components/SearchInput.vue new file mode 100644 index 00000000..a5d48b60 --- /dev/null +++ b/src/components/SearchInput.vue @@ -0,0 +1,12 @@ + + diff --git a/src/components/Segmented.vue b/src/components/Segmented.vue new file mode 100644 index 00000000..99073ff6 --- /dev/null +++ b/src/components/Segmented.vue @@ -0,0 +1,16 @@ + + diff --git a/src/components/SettingsRow.vue b/src/components/SettingsRow.vue new file mode 100644 index 00000000..de0159e2 --- /dev/null +++ b/src/components/SettingsRow.vue @@ -0,0 +1,12 @@ + + diff --git a/src/components/Sparkline.vue b/src/components/Sparkline.vue new file mode 100644 index 00000000..da03a8aa --- /dev/null +++ b/src/components/Sparkline.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/Stat.vue b/src/components/Stat.vue new file mode 100644 index 00000000..40607d99 --- /dev/null +++ b/src/components/Stat.vue @@ -0,0 +1,11 @@ + + diff --git a/src/components/StatusDot.vue b/src/components/StatusDot.vue new file mode 100644 index 00000000..15c28b20 --- /dev/null +++ b/src/components/StatusDot.vue @@ -0,0 +1,6 @@ + + diff --git a/src/components/Toggle.vue b/src/components/Toggle.vue new file mode 100644 index 00000000..c2bb51a4 --- /dev/null +++ b/src/components/Toggle.vue @@ -0,0 +1,8 @@ + + diff --git a/src/components/TweakRadio.vue b/src/components/TweakRadio.vue new file mode 100644 index 00000000..7c2721da --- /dev/null +++ b/src/components/TweakRadio.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/TweakSection.vue b/src/components/TweakSection.vue new file mode 100644 index 00000000..aee672dc --- /dev/null +++ b/src/components/TweakSection.vue @@ -0,0 +1,6 @@ + + diff --git a/src/components/TweaksPanel.vue b/src/components/TweaksPanel.vue new file mode 100644 index 00000000..6d829382 --- /dev/null +++ b/src/components/TweaksPanel.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/composables/bus.ts b/src/composables/bus.ts new file mode 100644 index 00000000..a6884090 --- /dev/null +++ b/src/composables/bus.ts @@ -0,0 +1,8 @@ +import mitt from 'mitt'; + +export interface RestoreItem { kind: string; text: string } +type Events = { + 'tvapp:restore-start': { items: RestoreItem[] }; + 'tvapp:restore-done': void; +}; +export const bus = mitt(); diff --git a/src/composables/useSettings.ts b/src/composables/useSettings.ts new file mode 100644 index 00000000..212a3c76 --- /dev/null +++ b/src/composables/useSettings.ts @@ -0,0 +1,34 @@ +import { ref, computed, reactive } from 'vue'; + +export const displayName = ref('TVApp2 Workspace'); +export const domain = ref('https://tvapp2.example.com'); +export const m3uPath = ref('/m3u/playlist.m3u8'); +export const epgPath = ref('/epg/guide.xml.gz'); + +export const m3uEndpoint = computed(() => `${domain.value.replace(/\/$/, '')}${m3uPath.value.startsWith('/') ? '' : '/'}${m3uPath.value}`); +export const epgEndpoint = computed(() => `${domain.value.replace(/\/$/, '')}${epgPath.value.startsWith('/') ? '' : '/'}${epgPath.value}`); + +export interface PlaylistStatus { + active: boolean; + endpointMode: 'global' | 'custom'; + customPath: string; +} + +const _status = reactive>({}); + +export function usePlaylistStatus(id: string): PlaylistStatus { + if (!_status[id]) { + _status[id] = { active: true, endpointMode: 'global', customPath: `/playlists/${id}.m3u` }; + } + return _status[id]; +} + +export function playlistEndpoint(id: string): string { + const s = usePlaylistStatus(id); + const base = domain.value.replace(/\/$/, ''); + if (s.endpointMode === 'custom') { + const p = s.customPath.startsWith('/') ? s.customPath : `/${s.customPath}`; + return `${base}${p}`; + } + return m3uEndpoint.value; +} diff --git a/src/composables/useTweaks.ts b/src/composables/useTweaks.ts new file mode 100644 index 00000000..00d140ce --- /dev/null +++ b/src/composables/useTweaks.ts @@ -0,0 +1,34 @@ +import { reactive, watch } from 'vue'; + +export interface Tweaks { + theme: 'dark' | 'light'; + density: 'compact' | 'regular' | 'spacious'; + epgMode: 'timeline' | 'list'; +} + +const tweaks = reactive({ + theme: 'dark', + density: 'regular', + epgMode: 'timeline', +}); + +watch( + () => [tweaks.theme, tweaks.density] as const, + ([theme, density]) => { + document.documentElement.dataset.theme = theme; + document.documentElement.dataset.density = density; + }, + { immediate: true } +); + +export function useTweaks() { + function setTweak(key: K, value: Tweaks[K]): void; + function setTweak(edits: Partial): void; + function setTweak(keyOrEdits: any, val?: any) { + const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null + ? keyOrEdits : { [keyOrEdits]: val }; + Object.assign(tweaks, edits); + try { window.parent?.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); } catch {} + } + return { tweaks, setTweak }; +} diff --git a/src/data.ts b/src/data.ts new file mode 100644 index 00000000..15ba099c --- /dev/null +++ b/src/data.ts @@ -0,0 +1,143 @@ +// Reactive store for TVApp2. +// +// Top-level data (PLAYLISTS, CHANNELS, ACTIVE_STREAMS, etc.) is fetched from +// the API at app startup via bootstrapData(). Consumers in + + diff --git a/src/screens/DashboardScreen.vue b/src/screens/DashboardScreen.vue new file mode 100644 index 00000000..05ee1a3e --- /dev/null +++ b/src/screens/DashboardScreen.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/screens/EPGDetailScreen.vue b/src/screens/EPGDetailScreen.vue new file mode 100644 index 00000000..7a6bd909 --- /dev/null +++ b/src/screens/EPGDetailScreen.vue @@ -0,0 +1,332 @@ + + + diff --git a/src/screens/EPGSourcesScreen.vue b/src/screens/EPGSourcesScreen.vue new file mode 100644 index 00000000..adf92e43 --- /dev/null +++ b/src/screens/EPGSourcesScreen.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/screens/HistoryMetricsScreen.vue b/src/screens/HistoryMetricsScreen.vue new file mode 100644 index 00000000..f9523274 --- /dev/null +++ b/src/screens/HistoryMetricsScreen.vue @@ -0,0 +1,393 @@ + + + diff --git a/src/screens/ImportScreen.vue b/src/screens/ImportScreen.vue new file mode 100644 index 00000000..6b848f6c --- /dev/null +++ b/src/screens/ImportScreen.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/screens/MappingScreen.vue b/src/screens/MappingScreen.vue new file mode 100644 index 00000000..5e601f95 --- /dev/null +++ b/src/screens/MappingScreen.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/screens/PlaylistDetailScreen.vue b/src/screens/PlaylistDetailScreen.vue new file mode 100644 index 00000000..d9995ebb --- /dev/null +++ b/src/screens/PlaylistDetailScreen.vue @@ -0,0 +1,527 @@ + + + diff --git a/src/screens/PlaylistsScreen.vue b/src/screens/PlaylistsScreen.vue new file mode 100644 index 00000000..96d4641a --- /dev/null +++ b/src/screens/PlaylistsScreen.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/screens/SettingsScreen.vue b/src/screens/SettingsScreen.vue new file mode 100644 index 00000000..32779606 --- /dev/null +++ b/src/screens/SettingsScreen.vue @@ -0,0 +1,369 @@ + + + diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts new file mode 100644 index 00000000..64c3fd9e --- /dev/null +++ b/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 00000000..8a8c5063 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,1762 @@ +/* ── Design tokens ────────────────────────────────────────────── */ +:root { + /* Dark (default) */ + --bg-0: oklch(0.16 0.005 240); + --bg-1: oklch(0.20 0.006 240); + --bg-2: oklch(0.24 0.008 240); + --bg-3: oklch(0.28 0.010 240); + --hairline: oklch(1 0 0 / 0.07); + --hairline-strong: oklch(1 0 0 / 0.12); + + --text-0: oklch(0.98 0 0); + --text-1: oklch(0.78 0.01 240); + --text-2: oklch(0.58 0.012 240); + --text-3: oklch(0.42 0.012 240); + + --accent: oklch(0.82 0.13 220); + --accent-hi: oklch(0.88 0.13 220); + --accent-soft: oklch(0.82 0.13 220 / 0.14); + --accent-glow: oklch(0.82 0.13 220 / 0.35); + + --good: oklch(0.78 0.16 150); + --warn: oklch(0.82 0.15 80); + --bad: oklch(0.70 0.18 25); + + --radius-s: 8px; + --radius-m: 12px; + --radius-l: 16px; + + /* Density (set per [data-density]) */ + --row-h: 44px; + --pad-card: 18px; + --pad-x: 16px; + --gap: 14px; + --fs-base: 13.5px; + --fs-sm: 12px; + --fs-xs: 11px; + --fs-h: 22px; + --fs-h2: 16px; +} + +[data-density="compact"] { + --row-h: 36px; + --pad-card: 14px; + --pad-x: 12px; + --gap: 10px; + --fs-base: 12.5px; + --fs-sm: 11.5px; + --fs-xs: 10.5px; + --fs-h: 19px; + --fs-h2: 14.5px; +} +[data-density="spacious"] { + --row-h: 52px; + --pad-card: 22px; + --pad-x: 20px; + --gap: 18px; + --fs-base: 14.5px; + --fs-sm: 13px; + --fs-xs: 11.5px; + --fs-h: 26px; + --fs-h2: 18px; +} + +[data-theme="light"] { + --bg-0: oklch(0.985 0.003 240); + --bg-1: oklch(1 0 0); + --bg-2: oklch(0.965 0.004 240); + --bg-3: oklch(0.94 0.005 240); + --hairline: oklch(0 0 0 / 0.08); + --hairline-strong: oklch(0 0 0 / 0.14); + --text-0: oklch(0.18 0.01 240); + --text-1: oklch(0.32 0.012 240); + --text-2: oklch(0.50 0.012 240); + --text-3: oklch(0.62 0.012 240); + + /* Darker semantic colors so foreground text stays legible on light backgrounds. + The originals (used in dark mode) are too washed out on white. */ + --accent: oklch(0.52 0.15 220); + --accent-hi: oklch(0.44 0.16 220); + --accent-soft: oklch(0.52 0.15 220 / 0.12); + --accent-glow: oklch(0.52 0.15 220 / 0.22); + --good: oklch(0.48 0.17 150); + --warn: oklch(0.55 0.17 60); + --bad: oklch(0.52 0.20 25); +} + +/* Hardcoded raw-oklch text colors that bypass the tokens — re-shade in light mode */ +[data-theme="light"] .log-line.log-error .log-msg { color: oklch(0.48 0.20 25); } +[data-theme="light"] .live-pill { color: oklch(0.48 0.20 25); background: oklch(0.52 0.20 25 / 0.12); border-color: oklch(0.52 0.20 25 / 0.4); } + +/* Builtin source/schedule icons use a dark navy gradient by default — swap to a + light surface in light mode so the row matches its siblings, while preserving + the cyan icon color and accent glow. */ +[data-theme="light"] .src-row .src-ico.builtin, +[data-theme="light"] .sched-ico.builtin { + background: var(--bg-3); + border-color: oklch(0.52 0.15 220 / 0.3); + box-shadow: inset 0 0 0 1px oklch(0.52 0.15 220 / 0.15), 0 0 18px oklch(0.52 0.15 220 / 0.18); +} +[data-theme="light"] .src-row .src-ico.builtin.epg-builtin, +[data-theme="light"] .src-ico.builtin.epg-builtin, +[data-theme="light"] .sched-ico.builtin.epg-builtin { + background: var(--bg-3); + border-color: oklch(0.48 0.17 150 / 0.3); + box-shadow: inset 0 0 0 1px oklch(0.48 0.17 150 / 0.15), 0 0 18px oklch(0.48 0.17 150 / 0.18); +} + +/* ── Reset / base ─────────────────────────────────────────────── */ +* { box-sizing: border-box; } +html, body, #root { height: 100%; margin: 0; } +body { + font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif; + font-size: var(--fs-base); + color: var(--text-0); + background: var(--bg-0); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.005em; +} +button { font: inherit; color: inherit; } +input, textarea, select { font: inherit; color: inherit; } +button, input, textarea, select { outline: none; } +::selection { background: var(--accent-soft); color: var(--text-0); } + +/* Scrollbars */ +*::-webkit-scrollbar { width: 10px; height: 10px; } +*::-webkit-scrollbar-track { background: transparent; } +*::-webkit-scrollbar-thumb { + background: var(--hairline-strong); + border-radius: 6px; + border: 2px solid transparent; + background-clip: content-box; +} +*::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: content-box; border: 2px solid transparent; } + +/* ── App shell ────────────────────────────────────────────────── */ +.app { + display: grid; + grid-template-columns: 224px 1fr; + height: 100vh; + min-width: 1100px; +} + +/* Sidebar */ +.sidebar { + background: var(--bg-0); + border-right: 1px solid var(--hairline); + padding: 20px 14px; + display: flex; + flex-direction: column; + gap: 18px; + overflow: hidden; +} +.brand { + padding: 4px 8px 8px; + font-weight: 700; + font-size: 19px; + letter-spacing: -0.02em; + color: var(--accent); + text-shadow: 0 0 24px var(--accent-glow); + display: flex; + align-items: center; + gap: 8px; +} +.brand-dot { + width: 8px; height: 8px; border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 12px var(--accent); +} +.nav-group-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-3); + padding: 6px 10px; + margin-top: 6px; +} +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 10px; + border-radius: var(--radius-s); + color: var(--text-1); + cursor: default; + font-size: var(--fs-base); + transition: background .12s, color .12s; + user-select: none; +} +.nav-item:hover { background: var(--bg-1); color: var(--text-0); } +.nav-item.active { + background: var(--accent-soft); + color: var(--accent-hi); + box-shadow: inset 0 0 0 1px oklch(0.82 0.13 220 / 0.25); +} +.nav-item .ico { width: 16px; height: 16px; flex: none; opacity: 0.9; } +.nav-item .count { + margin-left: auto; + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--text-3); + background: var(--bg-1); + padding: 1px 7px; + border-radius: 999px; +} +.nav-item.active .count { background: oklch(0.82 0.13 220 / 0.2); color: var(--accent-hi); } + +.sidebar-foot { + margin-top: auto; + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + border-radius: var(--radius-s); + background: var(--bg-1); + border: 1px solid var(--hairline); +} +.avatar { + width: 28px; height: 28px; border-radius: 50%; + background: linear-gradient(135deg, oklch(0.82 0.13 220), oklch(0.6 0.18 280)); + flex: none; + display: grid; place-items: center; + color: white; font-weight: 700; font-size: 11px; +} +.sidebar-foot .name { font-size: 12.5px; color: var(--text-0); } +.sidebar-foot .plan { font-size: 10.5px; color: var(--text-2); } + +/* Main */ +.main { + display: flex; + flex-direction: column; + background: var(--bg-0); + overflow: hidden; +} +.topbar { + height: 56px; + flex: none; + border-bottom: 1px solid var(--hairline); + display: flex; + align-items: center; + padding: 0 var(--pad-x); + gap: 12px; +} +.topbar h1 { + margin: 0; + font-size: var(--fs-h); + font-weight: 600; + letter-spacing: -0.018em; +} +.topbar .crumb { + color: var(--text-2); + font-size: var(--fs-sm); +} +.topbar-spacer { flex: 1; } + +/* Header restore-progress strip — spans from title to the theme toggle while a + "Restore defaults" job is running. Single line of what's being restored plus + a glowing cyan progress bar and overall percentage. */ +.restore-strip { + flex: 1; + min-width: 0; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + column-gap: 14px; + row-gap: 6px; + align-items: center; + margin: 0 18px 0 22px; + animation: fade-in .22s ease-out; +} +.restore-strip-line { + grid-column: 1; + grid-row: 1; + display: flex; + align-items: center; + gap: 8px; + font-size: var(--fs-sm); + color: var(--text-1); + white-space: nowrap; + overflow: hidden; + min-width: 0; +} +.restore-strip-line > svg { color: var(--accent-hi); flex: none; filter: drop-shadow(0 0 6px var(--accent-glow)); } +.restore-strip-action { + color: var(--accent-hi); + font-weight: 600; + letter-spacing: 0.01em; + flex: none; +} +.restore-strip-label { + color: var(--text-1); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; +} +.restore-strip-bar { + grid-column: 1; + grid-row: 2; + height: 4px; + background: var(--hairline-strong); + border-radius: 999px; + position: relative; + overflow: hidden; +} +.restore-strip-fill { + position: absolute; + inset: 0 auto 0 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--accent) 0%, var(--accent-hi) 60%, var(--accent) 100%); + background-size: 200% 100%; + box-shadow: 0 0 10px var(--accent), 0 0 22px var(--accent-glow); + transition: width .26s cubic-bezier(.4,.7,.3,1); + animation: restore-shimmer 1.6s linear infinite; +} +.restore-strip-fill::after { + content: ''; + position: absolute; + top: 0; right: 0; bottom: 0; + width: 18px; + background: linear-gradient(90deg, transparent, oklch(0.98 0.05 220 / 0.85)); + filter: blur(2px); +} +@keyframes restore-shimmer { + from { background-position: 0% 50%; } + to { background-position: 200% 50%; } +} +.restore-strip-pct { + grid-column: 2; + grid-row: 1 / span 2; + align-self: center; + font-size: 12px; + font-weight: 600; + color: var(--accent-hi); + letter-spacing: 0.02em; + text-shadow: 0 0 12px var(--accent-glow); + font-variant-numeric: tabular-nums; + min-width: 38px; + text-align: right; +} + +/* Theme toggle (segmented sun / moon pill) */ +.theme-toggle { + position: relative; + display: inline-flex; + align-items: center; + gap: 0; + width: 56px; + height: 28px; + padding: 0; + border-radius: 999px; + border: 1px solid var(--hairline); + background: var(--bg-2); + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease; +} +.theme-toggle:hover { border-color: var(--hairline-strong); } +.theme-toggle-ico { + flex: 1; + display: grid; + place-items: center; + color: var(--text-3); + z-index: 1; + pointer-events: none; +} +.theme-toggle-thumb { + position: absolute; + top: 2px; + width: 24px; + height: 24px; + border-radius: 50%; + display: grid; + place-items: center; + background: var(--bg-0); + color: var(--accent-hi); + border: 1px solid var(--hairline-strong); + box-shadow: 0 1px 2px rgba(0,0,0,0.25), 0 0 12px var(--accent-glow); + transition: left 0.22s cubic-bezier(.4,.7,.3,1), background 0.18s ease; + z-index: 2; +} +.theme-toggle-thumb.is-light { left: 2px; } +.theme-toggle-thumb.is-dark { left: 30px; } + +.screen { + flex: 1; + overflow-y: auto; + padding: var(--pad-x); +} + +/* ── Primitives ───────────────────────────────────────────────── */ +.card { + background: var(--bg-1); + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + padding: var(--pad-card); +} +.card.flush { padding: 0; } +.card-hd { + display: flex; + align-items: center; + gap: 10px; + padding: 14px var(--pad-card); + border-bottom: 1px solid var(--hairline); +} +.card-hd h2 { + margin: 0; + font-size: var(--fs-h2); + font-weight: 600; + letter-spacing: -0.01em; +} + +/* Button */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + height: 34px; + padding: 0 14px; + border-radius: var(--radius-s); + border: 1px solid var(--hairline); + background: var(--bg-1); + color: var(--text-0); + cursor: default; + font-size: var(--fs-sm); + font-weight: 500; + white-space: nowrap; + transition: background .12s, border-color .12s, color .12s, box-shadow .12s; + user-select: none; +} +.btn:hover { background: var(--bg-2); border-color: var(--hairline-strong); } +.btn:disabled, .btn[disabled] { + opacity: 0.42; + cursor: not-allowed; + pointer-events: none; + filter: saturate(0.4); + box-shadow: none; +} +.btn .ico { width: 14px; height: 14px; flex: none; } +.btn.primary { + background: linear-gradient(180deg, oklch(0.88 0.13 220), oklch(0.78 0.14 220)); + color: oklch(0.2 0.05 240); + border: 1px solid oklch(0.88 0.16 220 / 0.6); + box-shadow: + 0 0 0 1px oklch(0.92 0.1 220 / 0.4) inset, + 0 0 24px oklch(0.82 0.13 220 / 0.45), + 0 1px 0 oklch(1 0 0 / 0.4) inset; + font-weight: 600; +} +.btn.primary:hover { filter: brightness(1.05); } +.btn.ghost { background: transparent; border-color: transparent; color: var(--text-1); } +.btn.ghost:hover { background: var(--bg-1); color: var(--text-0); } +.btn.icon { padding: 0; width: 34px; justify-content: center; } +.btn.danger { color: var(--bad); } +.btn.danger:hover { background: oklch(0.7 0.18 25 / 0.1); border-color: oklch(0.7 0.18 25 / 0.3); } +.btn.sm { height: 28px; padding: 0 10px; font-size: var(--fs-xs); } +.btn.sm.icon { width: 28px; padding: 0; } + +/* Input */ +.input { + display: flex; + align-items: center; + gap: 8px; + height: 34px; + padding: 0 10px; + border-radius: var(--radius-s); + border: 1px solid var(--hairline); + background: var(--bg-1); + color: var(--text-0); + transition: border-color .12s, box-shadow .12s; +} +.input:focus-within { + border-color: oklch(0.82 0.13 220 / 0.5); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.input input, .input textarea { + flex: 1; + background: transparent; + border: 0; + padding: 0; + min-width: 0; +} +.input input::placeholder, .input textarea::placeholder { color: var(--text-3); } +.input .ico { width: 14px; height: 14px; color: var(--text-2); flex: none; } +.input.lg { height: 38px; } + +textarea.input { height: auto; padding: 10px; min-height: 80px; resize: vertical; } + +/* Select */ +.select { position: relative; } +.select select { + appearance: none; + -webkit-appearance: none; + height: 34px; + padding: 0 28px 0 10px; + border-radius: var(--radius-s); + border: 1px solid var(--hairline); + background: var(--bg-1); + color: var(--text-0); + font-size: var(--fs-sm); +} +.select::after { + content: ''; + position: absolute; + right: 10px; top: 50%; + width: 8px; height: 8px; + border-right: 1.5px solid var(--text-2); + border-bottom: 1.5px solid var(--text-2); + transform: translateY(-70%) rotate(45deg); + pointer-events: none; +} + +/* Toggle (cyan style from reference) */ +.toggle { + width: 38px; + height: 22px; + border-radius: 999px; + background: var(--bg-3); + border: 1px solid var(--hairline); + position: relative; + cursor: default; + flex: none; + transition: background .15s, box-shadow .15s, border-color .15s; +} +.toggle::after { + content: ''; + position: absolute; + top: 2px; left: 2px; + width: 16px; height: 16px; border-radius: 50%; + background: oklch(0.85 0 0); + box-shadow: 0 1px 2px rgba(0,0,0,0.4); + transition: left .15s, background .15s; +} +.toggle.on { + background: var(--accent); + border-color: oklch(0.88 0.16 220); + box-shadow: 0 0 20px var(--accent-glow); +} +.toggle.on::after { + left: 18px; + background: white; +} + +/* Checkbox (cyan filled) */ +.cbx { + width: 18px; height: 18px; + border-radius: 5px; + border: 1.5px solid var(--hairline-strong); + background: var(--bg-1); + flex: none; + display: grid; place-items: center; + cursor: default; + transition: background .12s, border-color .12s, box-shadow .12s; +} +.cbx.on { + background: var(--accent); + border-color: var(--accent); + box-shadow: 0 0 14px var(--accent-glow); +} +.cbx.on::after { + content: ''; + width: 5px; height: 9px; + border-right: 2px solid oklch(0.2 0.05 240); + border-bottom: 2px solid oklch(0.2 0.05 240); + transform: rotate(45deg) translate(-1px, -1px); +} + +/* Status dot */ +.dot { + width: 8px; height: 8px; + border-radius: 50%; + flex: none; + position: relative; +} +.dot.good { background: var(--good); box-shadow: 0 0 8px oklch(0.78 0.16 150 / 0.6); } +.dot.warn { background: var(--warn); box-shadow: 0 0 8px oklch(0.82 0.15 80 / 0.6); } +.dot.bad { background: var(--bad); box-shadow: 0 0 8px oklch(0.70 0.18 25 / 0.6); } +.dot.idle { background: var(--text-3); } +.dot.pulse::before { + content: ''; + position: absolute; inset: -3px; + border-radius: 50%; + background: inherit; + opacity: 0.5; + animation: pulse 1.6s ease-out infinite; +} +@keyframes pulse { + 0% { transform: scale(0.6); opacity: 0.5; } + 100% { transform: scale(2.2); opacity: 0; } +} + +/* Pill / tag */ +.pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 9px; + border-radius: 999px; + font-size: var(--fs-xs); + background: var(--bg-2); + color: var(--text-1); + border: 1px solid var(--hairline); + white-space: nowrap; +} +.pill.cyan { background: var(--accent-soft); color: var(--accent-hi); border-color: oklch(0.82 0.13 220 / 0.3); } +.pill.good { background: oklch(0.78 0.16 150 / 0.14); color: var(--good); border-color: oklch(0.78 0.16 150 / 0.3); } +.pill.warn { background: oklch(0.82 0.15 80 / 0.14); color: var(--warn); border-color: oklch(0.82 0.15 80 / 0.3); } +.pill.bad { background: oklch(0.70 0.18 25 / 0.14); color: var(--bad); border-color: oklch(0.70 0.18 25 / 0.3); } +.pill.active { + background: var(--accent-soft); + color: var(--accent-hi); + border-color: oklch(0.82 0.13 220 / 0.45); + box-shadow: 0 0 10px var(--accent-glow), inset 0 0 0 1px oklch(0.82 0.13 220 / 0.25); + text-shadow: 0 0 8px var(--accent-glow); +} +.pill.disabled { + background: oklch(0.72 0.17 55 / 0.14); + color: oklch(0.78 0.17 55); + border-color: oklch(0.72 0.17 55 / 0.4); +} + +/* Segmented control */ +.segmented { + display: inline-flex; + background: var(--bg-1); + border: 1px solid var(--hairline); + border-radius: var(--radius-s); + padding: 3px; + gap: 2px; +} +.segmented button { + background: transparent; + border: 0; + color: var(--text-2); + padding: 5px 10px; + border-radius: 6px; + cursor: default; + font-size: var(--fs-xs); + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 6px; + transition: background .12s, color .12s; +} +.segmented button:hover { color: var(--text-0); } +.segmented button.active { + background: var(--bg-3); + color: var(--accent-hi); + box-shadow: inset 0 0 0 1px var(--hairline-strong); +} + +/* Toolbar */ +.toolbar { + display: flex; + align-items: center; + gap: 10px; + padding: 12px var(--pad-card); + border-bottom: 1px solid var(--hairline); +} +.toolbar .spacer { flex: 1; } +.tbar-sep { + display: inline-block; + width: 1px; + height: 18px; + background: var(--hairline); + margin: 0 4px; + align-self: center; + flex: none; +} +.input.input-bad { + border-color: oklch(0.62 0.18 30 / 0.6); + box-shadow: 0 0 0 3px oklch(0.62 0.18 30 / 0.12); +} + +/* Toast surfaced after a custom-playlist Create/Append action completes */ +.custom-toast { + position: fixed; + left: 50%; + bottom: 28px; + transform: translateX(-50%); + z-index: 95; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 12px 10px 14px; + background: var(--bg-1); + border: 1px solid var(--hairline-strong); + border-radius: 999px; + box-shadow: 0 12px 36px rgba(0,0,0,0.35), 0 0 0 1px oklch(0.82 0.13 220 / 0.25) inset, 0 0 32px var(--accent-glow); + color: var(--text-0); + font-size: var(--fs-sm); + animation: custom-toast-in .26s cubic-bezier(.2,.7,.2,1); +} +.custom-toast > svg { color: var(--accent-hi); flex: none; } +.custom-toast-x { + display: inline-grid; + place-items: center; + width: 22px; + height: 22px; + border-radius: 50%; + border: 0; + background: var(--bg-3); + color: var(--text-2); + cursor: pointer; + margin-left: 2px; +} +.custom-toast-x:hover { background: var(--bg-0); color: var(--text-0); } +@keyframes custom-toast-in { + from { transform: translate(-50%, 12px); opacity: 0; } + to { transform: translate(-50%, 0); opacity: 1; } +} + +/* Table */ +.tbl { width: 100%; border-collapse: collapse; } +.tbl thead th { + text-align: left; + font-weight: 500; + font-size: var(--fs-xs); + color: var(--text-2); + padding: 10px var(--pad-card); + border-bottom: 1px solid var(--hairline); + background: var(--bg-1); + position: sticky; top: 0; + letter-spacing: 0.02em; + text-transform: uppercase; +} +.tbl tbody tr { + border-bottom: 1px solid var(--hairline); + transition: background .1s; +} +.tbl tbody tr:hover { background: var(--bg-2); } +.tbl tbody tr.selected { background: var(--accent-soft); } +.tbl tbody td { + padding: 10px var(--pad-card); + font-size: var(--fs-sm); + height: var(--row-h); + vertical-align: middle; +} + +/* Channel logo placeholder */ +.ch-logo { + width: 28px; height: 28px; + border-radius: 6px; + background: var(--bg-3); + display: grid; place-items: center; + font-size: 10px; + font-weight: 700; + color: var(--text-1); + letter-spacing: 0.02em; + flex: none; + border: 1px solid var(--hairline); + overflow: hidden; +} +.ch-logo.lg { width: 56px; height: 56px; font-size: 14px; border-radius: 10px; } + +/* Channel cards grid */ +.ch-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + gap: 12px; + padding: var(--pad-card); +} +.ch-card { + background: var(--bg-2); + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + padding: 14px; + display: flex; + flex-direction: column; + gap: 8px; + cursor: default; + transition: border-color .12s, background .12s, transform .12s; + position: relative; +} +.ch-card:hover { border-color: var(--hairline-strong); background: var(--bg-3); } +.ch-card.selected { border-color: oklch(0.82 0.13 220 / 0.55); box-shadow: 0 0 0 1px oklch(0.82 0.13 220 / 0.3), 0 0 28px var(--accent-glow); } +.ch-card .top { display: flex; align-items: flex-start; gap: 10px; } +.ch-card .name { font-weight: 600; font-size: var(--fs-sm); line-height: 1.25; } +.ch-card .meta { color: var(--text-2); font-size: var(--fs-xs); } +.ch-card .row { display: flex; align-items: center; justify-content: space-between; margin-top: auto; } +.ch-card .cbx-pos { position: absolute; top: 10px; right: 10px; opacity: 0; transition: opacity .12s; } +.ch-card:hover .cbx-pos, .ch-card.selected .cbx-pos { opacity: 1; } + +/* Stats row */ +.stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--gap); + margin-bottom: var(--gap); +} +.stat { padding: var(--pad-card); } +.stat .lbl { font-size: var(--fs-xs); color: var(--text-2); text-transform: uppercase; letter-spacing: 0.06em; } +.stat .val { font-size: 28px; font-weight: 600; letter-spacing: -0.02em; margin-top: 8px; font-variant-numeric: tabular-nums; } +.stat .delta { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs); margin-top: 6px; color: var(--good); } +.stat .delta.bad { color: var(--bad); } + +/* Source row (playlists / EPG list) */ +.src-row { + display: grid; + grid-template-columns: 40px 1fr auto auto auto auto; + align-items: center; + gap: 14px; + padding: 14px var(--pad-card); + border-bottom: 1px solid var(--hairline); + transition: background .1s; + cursor: default; +} +.src-row:last-child { border-bottom: 0; } +.src-row:hover { background: var(--bg-2); } +.src-row .src-ico { + width: 40px; height: 40px; + border-radius: 10px; + background: var(--bg-3); + display: grid; place-items: center; + color: var(--accent); + border: 1px solid var(--hairline); +} +.src-row .src-name { font-weight: 600; font-size: var(--fs-base); display: flex; align-items: center; gap: 8px; } +.src-row .src-url { color: var(--text-2); font-size: var(--fs-xs); font-family: 'JetBrains Mono', ui-monospace, monospace; margin-top: 3px; } +.src-row .stat-mini { font-size: var(--fs-xs); color: var(--text-2); text-align: right; min-width: 60px; } +.src-row .stat-mini b { color: var(--text-0); font-size: var(--fs-base); font-weight: 600; display: block; } + +/* EPG timeline */ +.epg { + display: grid; + grid-template-rows: 36px 1fr; + height: 100%; +} +.epg-head { + display: grid; + grid-template-columns: 200px 1fr; + border-bottom: 1px solid var(--hairline-strong); + position: sticky; top: 0; z-index: 4; + background: var(--bg-1); +} +.epg-head .head-l { border-right: 1px solid var(--hairline); display: flex; align-items: center; padding: 0 14px; font-size: var(--fs-xs); color: var(--text-2); text-transform: uppercase; letter-spacing: 0.06em; } +.epg-head .head-r { display: flex; position: relative; } +.epg-time { + flex: none; + font-size: var(--fs-xs); + color: var(--text-2); + padding: 0 8px; + border-right: 1px solid var(--hairline); + display: flex; + align-items: center; +} +.epg-body { overflow: auto; } +.epg-row { + display: grid; + grid-template-columns: 200px 1fr; + border-bottom: 1px solid var(--hairline); + min-height: 76px; +} +.epg-row .ch { + border-right: 1px solid var(--hairline); + display: flex; + align-items: center; + gap: 10px; + padding: 0 14px; + background: var(--bg-1); + position: sticky; left: 0; z-index: 2; +} +.epg-row .ch .nm { font-size: var(--fs-sm); font-weight: 500; } +.epg-row .ch .num { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; } +.epg-progs { position: relative; } +.epg-prog { + position: absolute; + top: 8px; bottom: 8px; + background: var(--bg-2); + border: 1px solid var(--hairline); + border-radius: 8px; + padding: 8px 11px; + font-size: var(--fs-xs); + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + gap: 3px; + cursor: default; + transition: background .1s, border-color .1s, transform .1s; +} +.epg-prog:hover { background: var(--bg-3); border-color: var(--hairline-strong); } +.epg-prog.live { + background: var(--accent-soft); + border-color: oklch(0.82 0.13 220 / 0.4); + color: var(--accent-hi); +} +.epg-prog .t { font-weight: 600; color: var(--text-0); line-height: 1.25; } +.epg-prog.live .t { color: var(--accent-hi); } +.epg-prog .sub { color: var(--text-2); font-size: 10.5px; line-height: 1.35; } +.now-line { + position: absolute; + top: 0; bottom: 0; + width: 2px; + background: var(--accent); + box-shadow: 0 0 12px var(--accent); + z-index: 3; + pointer-events: none; +} +.now-line::before { + content: ''; + position: absolute; + top: -3px; left: -4px; + width: 10px; height: 10px; + background: var(--accent); + border-radius: 50%; + box-shadow: 0 0 12px var(--accent); +} + +/* Drop zone */ +.dropzone { + border: 2px dashed var(--hairline-strong); + border-radius: var(--radius-l); + padding: 56px 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + text-align: center; + background: var(--bg-1); + cursor: default; + transition: border-color .15s, background .15s; +} +.dropzone:hover, .dropzone.over { + border-color: var(--accent); + background: var(--accent-soft); +} +.dropzone .icon-circle { + width: 56px; height: 56px; + border-radius: 50%; + background: var(--accent-soft); + display: grid; place-items: center; + color: var(--accent); +} +.dropzone h3 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; } +.dropzone p { margin: 0; color: var(--text-2); font-size: var(--fs-sm); } + +/* Player */ +.player { + background: black; + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + aspect-ratio: 16 / 9; + position: relative; + overflow: hidden; +} +.player .stripes { + position: absolute; inset: 0; + background: + repeating-linear-gradient(45deg, oklch(0.22 0.01 240) 0 12px, oklch(0.18 0.01 240) 12px 24px); + opacity: 0.5; +} +.player .label { + position: absolute; left: 12px; top: 10px; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 11px; + color: var(--text-2); + background: rgba(0,0,0,0.5); + padding: 3px 8px; + border-radius: 6px; + border: 1px solid var(--hairline); +} +.player .play { + position: absolute; inset: 0; + display: grid; place-items: center; +} +.player .play-btn { + width: 64px; height: 64px; + border-radius: 50%; + background: var(--accent); + display: grid; place-items: center; + box-shadow: 0 0 32px var(--accent-glow); + color: oklch(0.18 0.05 240); +} +.player .controls { + position: absolute; + bottom: 0; left: 0; right: 0; + height: 44px; + background: linear-gradient(180deg, transparent, rgba(0,0,0,0.7)); + display: flex; + align-items: center; + padding: 0 14px; + gap: 10px; + color: var(--text-1); +} +.player .controls .track { + flex: 1; + height: 3px; + background: var(--hairline-strong); + border-radius: 999px; + position: relative; +} +.player .controls .track::after { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 32%; + background: var(--accent); + border-radius: 999px; + box-shadow: 0 0 12px var(--accent); +} + +/* Section header */ +.section-title { + font-size: var(--fs-h2); + font-weight: 600; + margin: 0 0 var(--gap); + letter-spacing: -0.01em; + display: flex; + align-items: center; + gap: 10px; +} + +/* Mapping */ +.map-grid { + display: grid; + grid-template-columns: 1fr 40px 1fr; + gap: 14px; + align-items: stretch; +} +.map-col { + background: var(--bg-1); + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 480px; +} +.map-col h3 { + margin: 0; + font-size: var(--fs-sm); + font-weight: 600; + padding: 12px 14px; + border-bottom: 1px solid var(--hairline); + color: var(--text-1); + display: flex; + align-items: center; + gap: 8px; +} +.map-list { overflow-y: auto; flex: 1; } +.map-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-bottom: 1px solid var(--hairline); + cursor: default; + transition: background .1s; +} +.map-item:hover { background: var(--bg-2); } +.map-item.selected { background: var(--accent-soft); } +.map-item.matched { opacity: 0.55; } +.map-item .nm { font-size: var(--fs-sm); font-weight: 500; flex: 1; } +.map-item .id { font-size: var(--fs-xs); color: var(--text-2); font-family: 'JetBrains Mono', ui-monospace, monospace; } +.map-link { + display: grid; + place-items: center; + color: var(--text-3); +} +.match-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-bottom: 1px solid var(--hairline); + font-size: var(--fs-sm); +} +.match-row .arr { color: var(--accent); } + +/* Activity feed */ +.act { + display: flex; + gap: 12px; + padding: 12px var(--pad-card); + border-bottom: 1px solid var(--hairline); + font-size: var(--fs-sm); +} +.act:last-child { border-bottom: 0; } +.act .when { color: var(--text-3); font-size: var(--fs-xs); margin-top: 2px; font-variant-numeric: tabular-nums; } +.act .ico-w { + width: 28px; height: 28px; + border-radius: 50%; + background: var(--bg-2); + border: 1px solid var(--hairline); + display: grid; place-items: center; + color: var(--text-1); + flex: none; +} +.act b { font-weight: 600; } + +/* Sync grid (Settings → Syncing card) — every row shares the same 5 columns */ +.sync-grid { + display: flex; + flex-direction: column; + margin-top: 4px; +} +.sync-row { + display: grid; + grid-template-columns: 28px 1fr 180px 130px 44px; + align-items: center; + column-gap: 14px; + padding: 12px 0; + border-bottom: 1px solid var(--hairline); + transition: background .1s, opacity .12s; +} +.sync-row.sync-row-last, +.sync-row:last-child { border-bottom: 0; } +.sync-row.disabled { opacity: 0.5; } +.sync-row-lvl .sched-ico { display: none; } +.sync-row-lvl .sync-meta { grid-column: 1 / span 2; } +.sync-row-sched:hover { background: var(--bg-2); margin: 0 -10px; padding-left: 10px; padding-right: 10px; } +.sync-meta { min-width: 0; } +.sync-lbl { font-size: var(--fs-base); font-weight: 500; color: var(--text-0); } +.sync-hint { font-size: var(--fs-xs); color: var(--text-2); margin-top: 3px; } +.sync-col-sched, .sync-col-cron, .sync-col-toggle { display: flex; align-items: center; } +.sync-col-toggle { justify-content: flex-end; } +.sync-col-cron { justify-content: flex-end; } + +/* Section header rows span all 5 columns */ +.sync-section-hd { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 0 10px; + font-size: var(--fs-sm); + font-weight: 600; + color: var(--text-1); + border-bottom: 1px solid var(--hairline); + margin-top: 4px; +} +.sync-section-hd > .ico:first-child { color: var(--text-1); } +.sync-section-hd.collapsible { cursor: default; user-select: none; } +.sync-section-hd.collapsible:hover { color: var(--text-0); } +.sync-section-hd .chev { color: var(--text-2); transition: transform .18s ease; } +.sync-section-hd.collapsible.open .chev { transform: rotate(90deg); } + +/* Endpoint fields (Settings → General → Hosting endpoints) */ +.endpoint-field { + display: grid; + grid-template-columns: 160px 1fr auto; + align-items: center; + gap: 10px; +} +.endpoint-lbl { + display: flex; + align-items: center; + gap: 8px; + font-size: var(--fs-sm); + font-weight: 500; + color: var(--text-1); +} +.endpoint-ico { + width: 26px; height: 26px; + border-radius: 7px; + background: var(--bg-2); + border: 1px solid var(--hairline); + display: grid; place-items: center; +} + +.sched-ico { + width: 28px; height: 28px; + border-radius: 7px; + background: var(--bg-3); + border: 1px solid var(--hairline); + display: grid; place-items: center; + color: var(--text-1); +} +.sched-ico.is-epg { color: var(--good); } +.sched-ico.builtin { + background: linear-gradient(135deg, oklch(0.28 0.05 220), oklch(0.22 0.06 280)); + color: var(--accent-hi); + border-color: oklch(0.82 0.13 220 / 0.3); +} +.sched-ico.builtin.epg-builtin { + background: linear-gradient(135deg, oklch(0.28 0.08 150), oklch(0.22 0.08 150)); + color: var(--good); + border-color: oklch(0.78 0.16 150 / 0.35); +} + +.cron-chip { + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 10.5px; + padding: 4px 8px; + border-radius: 6px; + background: var(--bg-1); + border: 1px solid var(--hairline); + color: var(--accent-hi); + cursor: default; + letter-spacing: 0.02em; + transition: border-color .12s, background .12s; + white-space: nowrap; +} +.cron-chip:hover { border-color: var(--accent); background: var(--accent-soft); } + +/* No chevron on the schedule selectors — keeps row aligned with the cron chip */ +.sync-col-sched .select::after { display: none; } +.sync-col-sched .select select { padding-right: 12px; width: 100%; } + +/* Sidebar foot stack + Logs button */ +.sidebar-foot-stack { margin-top: auto; display: flex; flex-direction: column; gap: 8px; } +.logs-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: var(--radius-s); + background: var(--bg-1); + border: 1px solid var(--hairline); + color: var(--text-1); + font-size: var(--fs-sm); + cursor: default; + transition: background .12s, border-color .12s, color .12s; +} +.logs-btn:hover { background: var(--bg-2); border-color: var(--hairline-strong); color: var(--text-0); } +.logs-btn-ico { + position: relative; + width: 22px; height: 22px; + border-radius: 6px; + background: var(--bg-2); + border: 1px solid var(--hairline); + display: grid; place-items: center; + color: var(--text-1); +} +.logs-btn-ico .dot { position: absolute; top: -1px; right: -1px; } + +/* Realtime logs bottom drawer */ +.logs-drawer-wrap { position: fixed; inset: 0; z-index: 88; pointer-events: none; } +.logs-drawer-backdrop { + position: absolute; inset: 0; + pointer-events: auto; + animation: fade-in .18s ease-out; +} +.logs-drawer { + position: absolute; + left: 0; right: 0; bottom: 0; + height: 46vh; + min-height: 280px; + pointer-events: auto; + display: flex; + flex-direction: column; + border-bottom: 0; + border-left: 0; + border-right: 0; + animation: slidein-bottom .26s cubic-bezier(.2,.7,.2,1); +} +@keyframes slidein-bottom { from { transform: translateY(30px); opacity: 0; } to { transform: none; opacity: 1; } } + +.logs-hd { + flex: none; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-bottom: 1px solid var(--hairline); + background: linear-gradient(180deg, oklch(0 0 0 / 0.18), oklch(0 0 0 / 0)); +} +[data-theme="light"] .logs-hd { background: linear-gradient(180deg, oklch(1 0 0 / 0.4), oklch(1 0 0 / 0)); } +.logs-hd .row { flex: 1; } + +.logs-body { + flex: 1; + overflow-y: auto; + padding: 8px 0; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 11.5px; + line-height: 1.55; +} + +.log-line { + display: grid; + grid-template-columns: 90px 60px 60px 1fr; + gap: 12px; + padding: 3px 16px; + color: var(--text-1); + border-left: 2px solid transparent; +} +.log-line:hover { background: oklch(1 0 0 / 0.025); } +[data-theme="light"] .log-line:hover { background: oklch(0 0 0 / 0.03); } +.log-ts { color: var(--text-3); } +.log-lvl { font-weight: 600; font-size: 10.5px; letter-spacing: 0.04em; padding-top: 1px; } +.log-src { color: var(--text-2); } +.log-msg { color: var(--text-1); word-break: break-word; } + +.log-line.log-warn { border-left-color: oklch(0.82 0.15 80 / 0.5); background: oklch(0.82 0.15 80 / 0.04); } +.log-line.log-error { border-left-color: oklch(0.7 0.18 25 / 0.6); background: oklch(0.7 0.18 25 / 0.06); } +.log-line.log-error .log-msg { color: oklch(0.86 0.12 25); } +.log-line.log-ok { border-left-color: oklch(0.78 0.16 150 / 0.4); } +.log-line.log-debug { opacity: 0.7; } + +/* Mapping history rows */ +.hist-row { + display: flex; + align-items: flex-start; + gap: 14px; + padding: 12px var(--pad-card); + border-bottom: 1px solid var(--hairline); +} +.hist-row:last-child { border-bottom: 0; } +.hist-row:hover { background: var(--bg-2); } +.hist-ico { + width: 30px; height: 30px; + border-radius: 8px; + background: var(--bg-2); + border: 1px solid var(--hairline); + display: grid; place-items: center; + flex: none; + margin-top: 2px; +} + +/* History / Metrics screen */ +.hm-grid { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 14px; + min-height: 0; +} +.hm-list { overflow: hidden; } +.hm-detail { overflow-y: auto; max-height: 720px; } + +.qoe-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 9px 2px 7px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 600; + font-family: 'JetBrains Mono', ui-monospace, monospace; + border: 1px solid var(--hairline); +} +.qoe-pill[data-health="good"] { background: oklch(0.78 0.16 150 / 0.14); color: var(--good); border-color: oklch(0.78 0.16 150 / 0.3); } +.qoe-pill[data-health="good"] .dot { background: var(--good); box-shadow: 0 0 6px var(--good); } +.qoe-pill[data-health="warn"] { background: oklch(0.82 0.15 80 / 0.14); color: var(--warn); border-color: oklch(0.82 0.15 80 / 0.3); } +.qoe-pill[data-health="warn"] .dot { background: var(--warn); box-shadow: 0 0 6px var(--warn); } +.qoe-pill[data-health="bad"] { background: oklch(0.70 0.18 25 / 0.14); color: var(--bad); border-color: oklch(0.70 0.18 25 / 0.3); } +.qoe-pill[data-health="bad"] .dot { background: var(--bad); box-shadow: 0 0 6px var(--bad); } +.qoe-pill .dot { width: 6px; height: 6px; border-radius: 50%; } + +/* Buffer bar chart */ +.buf-bars { + display: flex; + align-items: flex-end; + gap: 3px; + height: 110px; + padding: 4px 0; +} +.buf-bar-wrap { flex: 1; height: 100%; display: flex; align-items: flex-end; } +.buf-bar { + width: 100%; + border-radius: 3px 3px 0 0; + min-height: 2px; +} + +/* Buffering timeline */ +.buf-timeline { + position: relative; + height: 32px; + background: var(--bg-2); + border-radius: 6px; + border: 1px solid var(--hairline); + overflow: hidden; +} +.buf-timeline-empty { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + color: var(--good); + font-size: 11px; +} +.buf-event { + position: absolute; + top: 4px; bottom: 4px; + background: var(--bad); + border-radius: 3px; + box-shadow: 0 0 8px oklch(0.7 0.18 25 / 0.6); + cursor: default; +} + +.logs-scroll-btn { + position: absolute; + left: 50%; + bottom: 14px; + transform: translateX(-50%); + background: var(--accent); + color: oklch(0.18 0.05 240); + border: 0; + padding: 6px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + cursor: default; + box-shadow: 0 4px 18px var(--accent-glow), 0 0 0 1px oklch(0.92 0.1 220 / 0.4) inset; + display: inline-flex; + align-items: center; + gap: 6px; + z-index: 5; +} + +/* Channel drawer */ +.drawer-wrap { position: fixed; inset: 0; z-index: 80; pointer-events: none; } +.drawer-backdrop { position: absolute; inset: 0; pointer-events: auto; animation: fade-in .2s ease-out; } +.drawer-panel { + position: absolute; + top: 0; right: 0; bottom: 0; + width: 440px; + pointer-events: auto; + display: flex; + flex-direction: column; + border-radius: 0; + border-right: 0; + border-top: 0; + border-bottom: 0; + animation: slidein-right .26s cubic-bezier(.2,.7,.2,1); +} +.drawer-hd { + padding: 16px 22px; + border-bottom: 1px solid var(--hairline); + display: flex; + align-items: center; + gap: 12px; + background: linear-gradient(180deg, oklch(0 0 0 / 0.15), oklch(0 0 0 / 0)); + flex: none; +} +[data-theme="light"] .drawer-hd { background: linear-gradient(180deg, oklch(1 0 0 / 0.4), oklch(1 0 0 / 0)); } +.drawer-body { + padding: 22px; + display: flex; + flex-direction: column; + gap: 14px; + overflow-y: auto; + flex: 1; +} + +/* \u2500\u2500 Glass panel surface (shared by side-panels + modals) \u2500\u2500\u2500\u2500\u2500\u2500\u2500 */ +.glass { + background: + linear-gradient(180deg, oklch(1 0 0 / 0.04), oklch(1 0 0 / 0) 40%), + oklch(0.20 0.006 240 / 0.72); + backdrop-filter: blur(28px) saturate(140%); + -webkit-backdrop-filter: blur(28px) saturate(140%); + border: 1px solid oklch(1 0 0 / 0.08); + box-shadow: + inset 0 1px 0 oklch(1 0 0 / 0.06), + inset 0 0 0 1px oklch(0.82 0.13 220 / 0.05), + -1px 0 0 oklch(0.82 0.13 220 / 0.08), + 0 30px 80px rgba(0,0,0,0.5), + 0 0 80px oklch(0.82 0.13 220 / 0.10); +} +[data-theme="light"] .glass { + background: + linear-gradient(180deg, oklch(1 0 0 / 0.7), oklch(1 0 0 / 0.5) 40%), + oklch(0.97 0.004 240 / 0.7); + border-color: oklch(0 0 0 / 0.08); + box-shadow: + inset 0 1px 0 oklch(1 0 0 / 0.6), + 0 30px 80px oklch(0.25 0.05 240 / 0.18), + 0 0 80px oklch(0.82 0.13 220 / 0.12); +} +.glass-bg { + background: oklch(0.1 0.005 240 / 0.45); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); +} +[data-theme="light"] .glass-bg { + background: oklch(0.6 0.01 240 / 0.32); +} + +/* Modal */ +.modal-bg { + position: fixed; inset: 0; + background: oklch(0.1 0.005 240 / 0.55); + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); + z-index: 90; + display: grid; place-items: center; + animation: fade-in .18s ease-out; +} +[data-theme="light"] .modal-bg { + background: oklch(0.6 0.01 240 / 0.36); +} +.modal { + width: 560px; + background: + linear-gradient(180deg, oklch(1 0 0 / 0.04), oklch(1 0 0 / 0) 40%), + oklch(0.20 0.006 240 / 0.78); + backdrop-filter: blur(32px) saturate(140%); + -webkit-backdrop-filter: blur(32px) saturate(140%); + border: 1px solid oklch(1 0 0 / 0.10); + border-radius: var(--radius-l); + box-shadow: + inset 0 1px 0 oklch(1 0 0 / 0.07), + 0 30px 80px rgba(0,0,0,0.55), + 0 0 100px oklch(0.82 0.13 220 / 0.18); + overflow: hidden; + animation: modal-in .24s cubic-bezier(.2,.7,.2,1); +} +[data-theme="light"] .modal { + background: + linear-gradient(180deg, oklch(1 0 0 / 0.75), oklch(1 0 0 / 0.55) 40%), + oklch(0.97 0.004 240 / 0.7); + border-color: oklch(0 0 0 / 0.08); + box-shadow: + inset 0 1px 0 oklch(1 0 0 / 0.7), + 0 30px 80px oklch(0.25 0.05 240 / 0.22), + 0 0 100px oklch(0.82 0.13 220 / 0.2); +} +.modal-hd { + padding: 18px 22px; + border-bottom: 1px solid var(--hairline); + display: flex; + align-items: center; + gap: 10px; + background: linear-gradient(180deg, oklch(1 0 0 / 0.02), transparent); +} +.modal-hd h2 { margin: 0; font-size: 16px; font-weight: 600; } +.modal-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; } +.modal-ft { + padding: 14px 22px; + border-top: 1px solid var(--hairline); + display: flex; + justify-content: flex-end; + gap: 8px; + background: oklch(0 0 0 / 0.18); +} +[data-theme="light"] .modal-ft { background: oklch(0 0 0 / 0.03); } +@keyframes modal-in { + from { transform: translateY(12px) scale(0.98); opacity: 0; } + to { transform: none; opacity: 1; } +} +.field-lbl { font-size: var(--fs-xs); color: var(--text-2); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; } + +/* Form */ +.form-row { display: flex; flex-direction: column; } +.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } + +/* Empty state */ +.empty { + padding: 60px 20px; + text-align: center; + color: var(--text-2); +} +.empty h3 { color: var(--text-0); margin: 0 0 6px; font-size: 16px; font-weight: 600; } + +/* Tweaks panel surface override (dark) */ +[data-theme="dark"] .twk-panel { + background: rgba(28, 30, 36, 0.85) !important; + color: var(--text-0) !important; + border-color: rgba(255,255,255,0.08) !important; + box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 60px oklch(0.82 0.13 220 / 0.05) !important; +} +[data-theme="dark"] .twk-panel * { color: inherit; } +[data-theme="dark"] .twk-lbl { color: var(--text-1) !important; } +[data-theme="dark"] .twk-val { color: var(--text-3) !important; } + +/* Active Streams */ +.streams-grid { + display: grid; + grid-template-columns: 380px 1fr; + gap: 14px; + height: 100%; + min-height: 0; +} +.streams-list { + display: flex; + flex-direction: column; + background: var(--bg-1); + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + overflow: hidden; + min-height: 0; +} +.streams-list .body { overflow-y: auto; flex: 1; } +.stream-item { + padding: 12px 14px; + display: grid; + grid-template-columns: 36px 1fr auto; + align-items: center; + gap: 12px; + border-bottom: 1px solid var(--hairline); + cursor: default; + transition: background .1s; + border-left: 2px solid transparent; +} +.stream-item:hover { background: var(--bg-2); } +.stream-item.selected { + background: var(--accent-soft); + border-left-color: var(--accent); +} +.stream-item .nm { font-weight: 500; font-size: var(--fs-sm); display: flex; align-items: center; gap: 8px; } +.stream-item .meta { font-size: var(--fs-xs); color: var(--text-2); margin-top: 3px; display: flex; align-items: center; gap: 8px; font-variant-numeric: tabular-nums; } +.stream-item .meta .mono { color: var(--text-1); } +.stream-item .viewer { + text-align: right; + font-variant-numeric: tabular-nums; +} +.stream-item .viewer b { font-size: var(--fs-base); font-weight: 600; color: var(--text-0); display: block; } +.stream-item .viewer span { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; } + +.stream-detail { + background: var(--bg-1); + border: 1px solid var(--hairline); + border-radius: var(--radius-m); + overflow-y: auto; + min-height: 0; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} +.metric { + padding: 12px 14px; + background: var(--bg-2); + border: 1px solid var(--hairline); + border-radius: 10px; +} +.metric .lbl { font-size: 10px; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.06em; } +.metric .val { font-size: 19px; font-weight: 600; letter-spacing: -0.01em; margin-top: 6px; font-variant-numeric: tabular-nums; } +.metric .sub { font-size: var(--fs-xs); color: var(--text-3); margin-top: 2px; } + +.kv-list { + display: grid; + grid-template-columns: 130px 1fr; + row-gap: 8px; + column-gap: 14px; + font-size: var(--fs-sm); +} +.kv-list .k { color: var(--text-2); } +.kv-list .v { color: var(--text-0); } + +.live-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px 3px 8px; + border-radius: 999px; + background: oklch(0.70 0.18 25 / 0.15); + color: oklch(0.78 0.18 25); + border: 1px solid oklch(0.70 0.18 25 / 0.35); + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.live-pill .dot { background: oklch(0.7 0.18 25); box-shadow: 0 0 8px oklch(0.7 0.18 25); } + +/* Stream view slide-over (1/2 screen) */ +.stream-view-bg { + position: fixed; + inset: 0; + z-index: 85; + background: oklch(0.1 0.005 240 / 0.45); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); + animation: fade-in .2s ease-out; +} +[data-theme="light"] .stream-view-bg { background: oklch(0.6 0.01 240 / 0.32); } +.stream-view { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 50vw; + min-width: 560px; + background: + linear-gradient(180deg, oklch(1 0 0 / 0.04), oklch(1 0 0 / 0) 40%), + oklch(0.20 0.006 240 / 0.74); + backdrop-filter: blur(30px) saturate(140%); + -webkit-backdrop-filter: blur(30px) saturate(140%); + border-left: 1px solid oklch(1 0 0 / 0.10); + box-shadow: + inset 1px 0 0 oklch(1 0 0 / 0.06), + -1px 0 0 oklch(0.82 0.13 220 / 0.18), + -30px 0 80px rgba(0,0,0,0.55), + -40px 0 120px oklch(0.82 0.13 220 / 0.12); + display: flex; + flex-direction: column; + animation: slidein-right .26s cubic-bezier(.2,.7,.2,1); +} +[data-theme="light"] .stream-view { + background: + linear-gradient(180deg, oklch(1 0 0 / 0.75), oklch(1 0 0 / 0.55) 40%), + oklch(0.97 0.004 240 / 0.7); + border-left-color: oklch(0 0 0 / 0.08); + box-shadow: + inset 1px 0 0 oklch(1 0 0 / 0.7), + -1px 0 0 oklch(0.82 0.13 220 / 0.22), + -30px 0 80px oklch(0.25 0.05 240 / 0.22), + -40px 0 120px oklch(0.82 0.13 220 / 0.15); +} +.stream-view-hd { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 22px; + border-bottom: 1px solid var(--hairline); + background: linear-gradient(180deg, oklch(0 0 0 / 0.15), oklch(0 0 0 / 0)); + flex: none; +} +[data-theme="light"] .stream-view-hd { + background: linear-gradient(180deg, oklch(1 0 0 / 0.4), oklch(1 0 0 / 0)); +} +.stream-view-body { + flex: 1; + overflow-y: auto; + padding: 18px 22px 28px; + display: flex; + flex-direction: column; + gap: 14px; +} +@keyframes slidein-right { from { transform: translateX(40px); opacity: 0; } to { transform: none; opacity: 1; } } +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } + +.player-ctrl { + background: transparent; + border: 0; + color: var(--text-1); + width: 28px; + height: 28px; + border-radius: 6px; + display: grid; + place-items: center; + cursor: default; + transition: background .12s, color .12s; +} +.player-ctrl:hover { background: rgba(255,255,255,0.08); color: white; } + +/* Built-in / system source badge */ +.pill.system { + background: linear-gradient(135deg, oklch(0.82 0.13 220 / 0.22), oklch(0.6 0.18 280 / 0.22)); + color: var(--accent-hi); + border-color: oklch(0.82 0.13 220 / 0.4); + font-weight: 600; + letter-spacing: 0.04em; +} +.src-row .src-ico.builtin { + background: linear-gradient(135deg, oklch(0.28 0.05 220), oklch(0.22 0.06 280)); + color: var(--accent-hi); + border-color: oklch(0.82 0.13 220 / 0.3); + box-shadow: inset 0 0 0 1px oklch(0.82 0.13 220 / 0.15), 0 0 18px oklch(0.82 0.13 220 / 0.15); +} +.src-row .src-ico.builtin.epg-builtin, +.src-ico.builtin.epg-builtin { + background: linear-gradient(135deg, oklch(0.28 0.08 150), oklch(0.22 0.08 150)); + color: var(--good); + border-color: oklch(0.78 0.16 150 / 0.35); + box-shadow: inset 0 0 0 1px oklch(0.78 0.16 150 / 0.2), 0 0 18px oklch(0.78 0.16 150 / 0.2); +} + +/* Utility */ +.row { display: flex; align-items: center; gap: 10px; } +.col { display: flex; flex-direction: column; gap: 10px; } +.muted { color: var(--text-2); } +.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; } +.divider { height: 1px; background: var(--hairline); margin: 8px 0; } +.spacer { flex: 1; } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..70578ecf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 00000000..85e3bf08 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/data.ts","./src/main.ts","./src/router.ts","./src/shims-vue.d.ts","./src/composables/bus.ts","./src/composables/usesettings.ts","./src/composables/usetweaks.ts","./src/app.vue","./src/components/addsourcemodal.vue","./src/components/btn.vue","./src/components/channelbulkdrawer.vue","./src/components/channeldrawer.vue","./src/components/channellogo.vue","./src/components/checkbox.vue","./src/components/endpointfield.vue","./src/components/hlsplayer.vue","./src/components/icon.vue","./src/components/logsdrawer.vue","./src/components/pill.vue","./src/components/playliststatusdrawer.vue","./src/components/searchinput.vue","./src/components/segmented.vue","./src/components/settingsrow.vue","./src/components/sparkline.vue","./src/components/stat.vue","./src/components/statusdot.vue","./src/components/toggle.vue","./src/components/tweakradio.vue","./src/components/tweaksection.vue","./src/components/tweakspanel.vue","./src/screens/activestreamsscreen.vue","./src/screens/dashboardscreen.vue","./src/screens/epgdetailscreen.vue","./src/screens/epgsourcesscreen.vue","./src/screens/historymetricsscreen.vue","./src/screens/importscreen.vue","./src/screens/mappingscreen.vue","./src/screens/playlistdetailscreen.vue","./src/screens/playlistsscreen.vue","./src/screens/settingsscreen.vue"],"version":"5.9.3"} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..6ac7a7d0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,15 @@ +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, + }, + }, + }, +});