initial push from external dev branches

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

116
server/dist/sources/adapters/dulo.js vendored Normal file
View File

@@ -0,0 +1,116 @@
// dulo.tv source adapter (Phase 1). Ported from ../d-combine/sources/dulo/adapter.mjs.
//
// dulo.tv exposes a JSON catalog API and each channel's `source_url` IS a token-free HLS master
// playlist. The memfs hosts gate playback behind an Origin allowlist, so the proxy injects
// `Origin: https://dulo.tv` on every hop. No server-side resolve is needed.
import { readFileSync } from 'node:fs';
import { snapshotFile } from '../paths.js';
const SNAPSHOT = snapshotFile('dulo');
const DULO_ORIGIN = 'https://dulo.tv';
const DULO_API = process.env.DULO_API || 'https://dulo.tv/api/live-tv/channels';
function isHttpUrl(url) {
if (typeof url !== 'string')
return false;
try {
const u = new URL(url);
return u.protocol === 'https:' || u.protocol === 'http:';
}
catch {
return false;
}
}
function toIso(ts) {
if (!ts || typeof ts !== 'string')
return null;
const d = new Date(ts);
return Number.isNaN(d.getTime()) ? null : d.toISOString();
}
const duloAdapter = {
id: 'dulo',
label: 'dulo',
// Prefer the live catalog API; fall back to the captured snapshot when offline / region-blocked.
async listChannels() {
try {
const res = await fetch(DULO_API, { headers: { Origin: DULO_ORIGIN } });
if (!res.ok)
throw new Error(`HTTP ${res.status}`);
const body = (await res.json());
const raw = body.channels || [];
if (!raw.length)
throw new Error('empty channel list');
return { raw, meta: { endpoint: DULO_API, live: true, fetchedAt: new Date().toISOString() } };
}
catch (err) {
const snap = JSON.parse(readFileSync(SNAPSHOT, 'utf8'));
return {
raw: snap.channels || [],
meta: {
endpoint: DULO_API,
live: false,
fallback: 'dulo.snapshot.json',
reason: err.message,
fetchedAt: new Date().toISOString(),
},
};
}
},
normalize(raw, { ingestedAt }) {
const sourceChannelId = String(raw.id);
const category = raw.category || null;
return {
_id: `dulo:${sourceChannelId}`,
source: 'dulo',
sourceChannelId,
name: raw.name,
category, // dulo has real semantic categories
groupKey: category || 'uncategorized',
groupLabel: category || 'uncategorized',
logoUrl: raw.logo_url || null,
streamEntryUrl: raw.source_url, // token-free master .m3u8 (handed straight to the proxy)
isPlayable: isHttpUrl(raw.source_url),
sourceCreatedAt: toIso(raw.created_at),
sourceUpdatedAt: toIso(raw.updated_at),
ingestedAt,
};
},
grouping: { by: 'groupKey', groupOrder: 'alpha', channelOrder: 'name' },
isEntryUrl() {
return false; // dulo source_url is already the master — nothing to resolve
},
async resolveStream(entryUrl) {
return { masterUrl: entryUrl }; // identity no-op
},
proxy: {
upstreamHeaders() {
return { Origin: DULO_ORIGIN }; // the memfs Origin allowlist gate
},
isAllowedUpstream(url) {
try {
const u = new URL(url);
return (u.protocol === 'https:' || u.protocol === 'http:') && u.hostname.endsWith('.dulo.tv');
}
catch {
return false;
}
},
onPlaylistChildHost: null, // static allowlist — nothing to learn at runtime
relabelSegmentContentType(_url, contentType) {
return contentType || 'application/octet-stream'; // plain TS — pass the upstream type through
},
classifyArtifact(url) {
try {
const p = new URL(url).pathname.toLowerCase();
if (p.endsWith('.ts'))
return 'segment';
if (p.endsWith('.m3u8'))
return p.includes('_output_') ? 'variant' : 'master';
return 'other';
}
catch {
return 'other';
}
},
},
};
export default duloAdapter;
//# sourceMappingURL=dulo.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dulo.js","sourceRoot":"","sources":["../../../src/sources/adapters/dulo.ts"],"names":[],"mappings":"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"}

39
server/dist/sources/core/buildSource.js vendored Normal file
View File

@@ -0,0 +1,39 @@
// The generic "standard function" pipeline, run once per source for a LIVE sync:
// listChannels() → raw upstream records → normalize(raw) → one SourceChannel doc each
// → dedupe by deterministic _id (last wins, idempotent) → return docs for upsert.
//
// Ported from d-combine/lib/core/build-source.mjs, but returns the docs instead of writing files —
// the seed module upserts them into Mongo. The whole file is source-agnostic.
import { logger } from './logger.js';
export async function buildSource(adapter) {
const startedAt = Date.now();
logger.info('build', `[${adapter.id}] fetching channel listings…`);
const { raw, meta = {} } = await adapter.listChannels();
logger.info('build', `[${adapter.id}] got ${raw.length} raw records (${meta.live === false ? 'offline snapshot' : 'live'})`);
const ingestedAt = new Date().toISOString();
const normalized = [];
for (const record of raw) {
try {
const doc = adapter.normalize(record, { ingestedAt });
if (doc)
normalized.push(doc);
}
catch (err) {
logger.warn('build', `[${adapter.id}] skipped a record: ${err.message}`);
}
}
// Dedupe by deterministic _id (last wins) — guards against duplicate upstream rows.
const byId = new Map();
for (const doc of normalized)
byId.set(doc._id, doc);
const docs = [...byId.values()];
logger.ok('build', `[${adapter.id}] normalized ${docs.length} docs`);
return {
id: adapter.id,
count: docs.length,
docs,
live: meta.live !== false,
meta: { ...meta, buildMs: Date.now() - startedAt },
};
}
//# sourceMappingURL=buildSource.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"buildSource.js","sourceRoot":"","sources":["../../../src/sources/core/buildSource.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,yFAAyF;AACzF,oFAAoF;AACpF,EAAE;AACF,mGAAmG;AACnG,8EAA8E;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAYrC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAsB;IACtD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,8BAA8B,CAAC,CAAC;IAEnE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IACxD,MAAM,CAAC,IAAI,CACT,OAAO,EACP,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,MAAM,iBAAiB,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,GAAG,CACvG,CAAC;IAEF,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,UAAU,GAAuB,EAAE,CAAC;IAC1C,KAAK,MAAM,MAAM,IAAI,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;YACtD,IAAI,GAAG;gBAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,uBAAwB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;IACjD,KAAK,MAAM,GAAG,IAAI,UAAU;QAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAEhC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAAE,gBAAgB,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;IACrE,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,KAAK,EAAE,IAAI,CAAC,MAAM;QAClB,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,KAAK;QACzB,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE;KACnD,CAAC;AACJ,CAAC"}

18
server/dist/sources/core/logger.js vendored Normal file
View File

@@ -0,0 +1,18 @@
// Tiny tagged logger matching the existing console style ("[mongo] connected", "[api] …").
// Ported from d-combine/lib/core/logger.mjs, trimmed to what the core uses.
function emit(level, tag, msg) {
const line = `[${tag}] ${msg}`;
if (level === 'error')
console.error(line);
else if (level === 'warn')
console.warn(line);
else
console.info(line);
}
export const logger = {
info: (tag, msg) => emit('info', tag, msg),
warn: (tag, msg) => emit('warn', tag, msg),
error: (tag, msg) => emit('error', tag, msg),
ok: (tag, msg) => emit('ok', tag, msg),
};
//# sourceMappingURL=logger.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../src/sources/core/logger.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,4EAA4E;AAI5E,SAAS,IAAI,CAAC,KAAY,EAAE,GAAW,EAAE,GAAW;IAClD,MAAM,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;IAC/B,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SACtC,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;QACzC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC;IAC1D,IAAI,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC;IAC1D,KAAK,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC;IAC5D,EAAE,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC;CACvD,CAAC"}

35
server/dist/sources/core/metrics.js vendored Normal file
View File

@@ -0,0 +1,35 @@
// In-memory counters, one set PER SOURCE. Surfaced via /api/sources/:id/status and /health-style
// reporting. Ported from d-combine/lib/core/metrics.mjs.
export function createMetrics() {
return {
startedAt: Date.now(),
requests: { total: 0, master: 0, variant: 0, segment: 0, other: 0, errors: 0 },
upstream: { ok: 0, notLive: 0, forbidden: 0, failed: 0 }, // 404=notLive, 403=forbidden(gate)
bytesStreamed: 0,
active: 0,
lastStreamAt: null,
lastError: null,
};
}
/** Human-readable byte size. */
export function fmtBytes(n) {
if (n < 1024)
return `${n}B`;
if (n < 1048576)
return `${(n / 1024).toFixed(1)}KB`;
return `${(n / 1048576).toFixed(2)}MB`;
}
/** JSON snapshot of one source's metrics. */
export function snapshotOne(m) {
return {
uptimeSeconds: Math.round((Date.now() - m.startedAt) / 1000),
active: m.active,
requests: { ...m.requests },
upstream: { ...m.upstream },
bytesStreamed: m.bytesStreamed,
mbStreamed: +(m.bytesStreamed / 1048576).toFixed(2),
lastStreamAt: m.lastStreamAt ? new Date(m.lastStreamAt).toISOString() : null,
lastError: m.lastError,
};
}
//# sourceMappingURL=metrics.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/sources/core/metrics.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,yDAAyD;AAmBzD,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;QAC9E,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,mCAAmC;QAC7F,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,CAAC;QACT,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;KAChB,CAAC;AACJ,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,IAAI,CAAC,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC;IAC7B,IAAI,CAAC,GAAG,OAAO;QAAE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,OAAO,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,OAAO;QACL,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QAC5D,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC3B,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC3B,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACnD,YAAY,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QAC5E,SAAS,EAAE,CAAC,CAAC,SAAS;KACvB,CAAC;AACJ,CAAC"}

43
server/dist/sources/core/playlist.js vendored Normal file
View File

@@ -0,0 +1,43 @@
// Shared HLS playlist helpers. Ported from d-combine/lib/core/playlist.mjs. The only per-source
// difference — whether the rewriter also learns (allowlists) each child host — is the `onChildHost`
// hook param, so one implementation serves every source.
/** True if the upstream URL / content-type looks like an HLS playlist (.m3u8). */
export function looksLikePlaylist(upstreamUrl, contentType) {
if (contentType && contentType.includes('mpegurl'))
return true; // apple.mpegurl / x-mpegurl
try {
return new URL(upstreamUrl).pathname.toLowerCase().endsWith('.m3u8');
}
catch {
return false;
}
}
/**
* Rewrite every child URI in a playlist so it routes back through this proxy.
*
* @param text the raw playlist body
* @param baseUrl the upstream URL it was fetched from (for relative→absolute)
* @param prefix proxy mount prefix to prepend, e.g. "/api/v1/dulo/"
* @param onChildHost per-child-host hook (dlhd dynamic-allow; dulo/common null)
*/
export function rewritePlaylist(text, baseUrl, prefix, onChildHost) {
return text
.split(/\r?\n/)
.map((rawLine) => {
const trimmed = rawLine.trim();
if (!trimmed || trimmed.startsWith('#'))
return rawLine; // tag / comment / blank → as-is
const abs = new URL(trimmed, baseUrl).href; // resolve relative → absolute
if (onChildHost) {
try {
onChildHost(new URL(abs).hostname);
}
catch {
/* ignore malformed */
}
}
return `${prefix}${encodeURIComponent(abs)}`;
})
.join('\n');
}
//# sourceMappingURL=playlist.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"playlist.js","sourceRoot":"","sources":["../../../src/sources/core/playlist.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,oGAAoG;AACpG,yDAAyD;AAEzD,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,WAAmB,EAAE,WAAmB;IACxE,IAAI,WAAW,IAAI,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,4BAA4B;IAC7F,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,OAAe,EACf,MAAc,EACd,WAA4C;IAE5C,OAAO,IAAI;SACR,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACf,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,CAAC,gCAAgC;QACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,8BAA8B;QAC1E,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,WAAW,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;QACD,OAAO,GAAG,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/C,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}

154
server/dist/sources/core/proxyHandler.js vendored Normal file
View File

@@ -0,0 +1,154 @@
// One Express handler factory per source, bound to GET /api/v1/:source/*. Ported from
// d-combine/lib/core/proxy-handler.mjs. Every per-source difference (resolve, headers, SSRF allow,
// dynamic-allow, segment relabel, artifact classification) is read off `adapter`; the control flow
// below is invariant.
//
// /api/v1/<source>/<enc entry-or-stream URL>
// · entry URL (dlhd watch.php / stream-N.php) → adapter.resolveStream() → fresh master, then proxy
// · master/variant .m3u8 → rewrite child URIs back through /api/v1/<source>/…
// · segment → pipe bytes (adapter may relabel the content-type)
import { Readable } from 'node:stream';
import { logger } from './logger.js';
import { fmtBytes } from './metrics.js';
import { looksLikePlaylist, rewritePlaylist } from './playlist.js';
function label(url) {
try {
const u = new URL(url);
const file = u.pathname.split('/').pop() || '';
return { host: u.hostname, short: file.slice(0, 8) || '/' };
}
catch {
return { host: '?', short: '?' };
}
}
export function createProxyHandler(adapter, metrics) {
// Marker used to slice the raw (still-encoded) upstream URL out of req.originalUrl, independent of
// where the router is mounted. Keeps embedded ?session=/?md5&expires through ONE decodeURIComponent.
const MARKER = `/v1/${adapter.id}/`;
const PREFIX = `/api/v1/${adapter.id}/`;
const tag = `stream:${adapter.id}`;
const { proxy } = adapter;
return async function handler(req, res) {
const startedAt = Date.now();
const ms = () => `${Date.now() - startedAt}ms`;
// 1. Extract + decode the single percent-encoded upstream URL segment.
const idx = req.originalUrl.indexOf(MARKER);
const rawPath = idx >= 0 ? req.originalUrl.slice(idx + MARKER.length) : '';
let upstreamUrl;
try {
upstreamUrl = decodeURIComponent(rawPath);
}
catch {
logger.warn(tag, `400 malformed encoded URL from ${req.ip}`);
res.status(400).type('text/plain').send('Bad request: malformed encoded URL');
return;
}
// 2. Resolve-then-proxy: an entry URL must become a fresh stream URL first
// (dulo/common: never — entry IS the master; dlhd: the 3-hop scrape).
if (adapter.isEntryUrl(upstreamUrl)) {
metrics.requests.total++;
metrics.requests.master++;
try {
const resolved = await adapter.resolveStream(upstreamUrl);
upstreamUrl = resolved.masterUrl;
}
catch (err) {
metrics.requests.errors++;
metrics.upstream.notLive++;
metrics.lastError = err.message;
logger.warn(tag, `resolve failed: ${err.message} (${ms()})`);
res.status(502).type('text/plain').send(`Resolve failed: ${err.message}`);
return;
}
}
else {
// 3. Direct hop (master/variant/segment from a rewritten playlist) → SSRF gate.
if (!proxy.isAllowedUpstream(upstreamUrl)) {
logger.warn(tag, `400 blocked upstream: ${String(upstreamUrl).slice(0, 80)}`);
res.status(400).type('text/plain').send('Bad request: upstream host not in the allowlist');
return;
}
metrics.requests.total++;
metrics.requests[proxy.classifyArtifact(upstreamUrl)]++;
}
const type = proxy.classifyArtifact(upstreamUrl);
const { host, short } = label(upstreamUrl);
metrics.active++;
res.on('close', () => {
metrics.active--;
});
let upstream;
try {
upstream = await fetch(upstreamUrl, { headers: proxy.upstreamHeaders(upstreamUrl) });
}
catch (err) {
metrics.upstream.failed++;
metrics.requests.errors++;
metrics.lastError = err.message;
logger.error(tag, `${type} ${host} ${short} upstream fetch failed: ${err.message} (${ms()})`);
res.status(502).type('text/plain').send(`Upstream fetch failed: ${err.message}`);
return;
}
// Forward upstream errors verbatim. 404 = not transcoding right now; 403 = origin/referer gate.
if (!upstream.ok) {
metrics.requests.errors++;
if (upstream.status === 404)
metrics.upstream.notLive++;
else if (upstream.status === 403)
metrics.upstream.forbidden++;
else
metrics.upstream.failed++;
const note = upstream.status === 404 ? ' (not live)' : upstream.status === 403 ? ' (gate)' : '';
metrics.lastError = `HTTP ${upstream.status} ${host}/${short}`;
logger.warn(tag, `${type} ${host} ${short} status=${upstream.status}${note} (${ms()})`);
const detail = await upstream.text().catch(() => '');
res
.status(upstream.status)
.type(upstream.headers.get('content-type') || 'text/plain')
.send(detail || `Upstream HTTP ${upstream.status}`);
return;
}
const contentType = upstream.headers.get('content-type') || '';
// 4. Playlist → rewrite child URIs back through this source's proxy prefix
// (and let the adapter learn each child host: dlhd dynamic-allow; dulo/common no-op).
if (looksLikePlaylist(upstreamUrl, contentType)) {
const rewritten = rewritePlaylist(await upstream.text(), upstreamUrl, PREFIX, proxy.onPlaylistChildHost);
const bytes = Buffer.byteLength(rewritten);
metrics.upstream.ok++;
metrics.bytesStreamed += bytes;
metrics.lastStreamAt = Date.now();
logger.ok(tag, `${type} ${host} ${short} status=200 ${fmtBytes(bytes)} (${ms()})`);
res.set('Cache-Control', 'no-store'); // playlists + tokens are short-lived
res.type('application/vnd.apple.mpegurl').send(rewritten);
return;
}
// 5. Segment (or anything else) → stream the bytes through, content-type per the adapter.
res.set('Content-Type', proxy.relabelSegmentContentType(upstreamUrl, contentType, type));
res.set('Cache-Control', 'no-store');
if (!upstream.body) {
metrics.upstream.ok++;
logger.ok(tag, `${type} ${host} ${short} status=200 0B (${ms()})`);
res.end();
return;
}
let bytes = 0;
const body = Readable.fromWeb(upstream.body);
body.on('data', (chunk) => {
bytes += chunk.length;
});
res.on('finish', () => {
metrics.upstream.ok++;
metrics.bytesStreamed += bytes;
metrics.lastStreamAt = Date.now();
logger.ok(tag, `${type} ${host} ${short} status=200 ${fmtBytes(bytes)} (${ms()})`);
});
body.on('error', (err) => {
metrics.upstream.failed++;
metrics.lastError = err.message;
logger.error(tag, `${type} ${host} ${short} stream error: ${err.message}`);
res.destroy(err);
});
body.pipe(res);
};
}
//# sourceMappingURL=proxyHandler.js.map

File diff suppressed because one or more lines are too long

16
server/dist/sources/paths.js vendored Normal file
View File

@@ -0,0 +1,16 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// This module lives at <root>/sources/paths.{ts,js} in BOTH dev (server/src) and prod (server/dist).
// The bundled seed assets sit at server/seed-data/sources — i.e. two levels up from here, then
// seed-data/sources. The Docker runtime stage copies server/seed-data → /app/seed-data alongside
// /app/dist, so this same relative resolution holds in the container.
const here = dirname(fileURLToPath(import.meta.url));
/** Directory holding the committed <id>.source.json baselines + <id>.snapshot.json fallbacks. */
export const SEED_SOURCES_DIR = resolve(here, '..', '..', 'seed-data', 'sources');
export function bundleFile(sourceId) {
return resolve(SEED_SOURCES_DIR, `${sourceId}.source.json`);
}
export function snapshotFile(sourceId) {
return resolve(SEED_SOURCES_DIR, `${sourceId}.snapshot.json`);
}
//# sourceMappingURL=paths.js.map

1
server/dist/sources/paths.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/sources/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,qGAAqG;AACrG,+FAA+F;AAC/F,iGAAiG;AACjG,sEAAsE;AACtE,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErD,iGAAiG;AACjG,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;AAElF,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,OAAO,CAAC,gBAAgB,EAAE,GAAG,QAAQ,cAAc,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,OAAO,CAAC,gBAAgB,EAAE,GAAG,QAAQ,gBAAgB,CAAC,CAAC;AAChE,CAAC"}

10
server/dist/sources/registry.js vendored Normal file
View File

@@ -0,0 +1,10 @@
// The single enumeration of source adapters TVApp2 knows about. Ported from
// ../d-combine/sources/registry.mjs. Adding a source (Phase 2: common, Phase 3: dlhd) = write a new
// adapter under adapters/ and add it here; the boot init, sources router (manifest + proxy mounts),
// and SPA all iterate this list, so nothing else needs to change.
import duloAdapter from './adapters/dulo.js';
export const SOURCES = [duloAdapter];
export function getSource(id) {
return SOURCES.find((s) => s.id === id);
}
//# sourceMappingURL=registry.js.map

1
server/dist/sources/registry.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sources/registry.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,oGAAoG;AACpG,oGAAoG;AACpG,kEAAkE;AAElE,OAAO,WAAW,MAAM,oBAAoB,CAAC;AAG7C,MAAM,CAAC,MAAM,OAAO,GAAoB,CAAC,WAAW,CAAC,CAAC;AAEtD,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1C,CAAC"}

137
server/dist/sources/seed.js vendored Normal file
View File

@@ -0,0 +1,137 @@
// Seed / init / sync / reset for the established (Default) source playlists.
//
// "Both: bundle + live sync" — the committed <id>.source.json bundle is the GUARANTEED baseline used
// to initialize/reset MongoDB (offline-safe, reproducible); a live sync then refreshes channels and
// the Playlist's sync metadata from upstream when reachable. Boot init (ensureSeeded + background
// syncLive via bootInitSources) runs once per source in server/src/index.ts after the Mongo connect,
// covering both Docker variants without extra orchestration.
import { readFileSync } from 'node:fs';
import { Playlist } from '../models/Playlist.js';
import { SourceChannel } from '../models/SourceChannel.js';
import { SOURCES, getSource } from './registry.js';
import { buildSource } from './core/buildSource.js';
import { bundleFile } from './paths.js';
import { logger } from './core/logger.js';
function readBundle(id) {
const arr = JSON.parse(readFileSync(bundleFile(id), 'utf8'));
if (!Array.isArray(arr))
throw new Error(`bundle for "${id}" is not a JSON array`);
return arr;
}
function groupCount(docs) {
return new Set(docs.map((d) => d.groupKey)).size;
}
// Idempotent upsert by _id. Strips _id from $set (immutable; supplied by the filter on insert).
async function upsertChannels(docs) {
if (!docs.length)
return;
const ops = docs.map((d) => {
const { _id, ...rest } = d;
return { updateOne: { filter: { _id }, update: { $set: rest }, upsert: true } };
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await SourceChannel.bulkWrite(ops, { ordered: false });
}
async function upsertPlaylistRow(adapter, groups, opts) {
const row = {
id: adapter.id,
source: adapter.id,
name: `(Default) ${adapter.label}`,
url: `source://${adapter.id}`,
groups,
lastSync: opts.lastSync,
status: opts.status,
auto: true,
interval: 'Auto-updated',
builtin: true,
};
await Playlist.updateOne({ id: adapter.id }, { $set: row }, { upsert: true });
}
export async function validateIntegrity(id) {
const issues = [];
const playlist = await Playlist.findOne({ id }).lean();
const channelCount = await SourceChannel.countDocuments({ source: id });
if (!playlist)
issues.push('playlist row missing');
if (channelCount === 0)
issues.push('no channels');
const sample = await SourceChannel.findOne({ source: id }).lean();
if (sample) {
for (const f of ['name', 'streamEntryUrl', 'groupKey']) {
if (!sample[f])
issues.push(`a channel is missing required field "${f}"`);
}
}
return { id, playlistExists: !!playlist, channelCount, ok: issues.length === 0, issues };
}
/** Initialize a source from its committed bundle (idempotent upsert). */
export async function initFromBundle(id) {
const adapter = getSource(id);
if (!adapter)
throw new Error(`unknown source: ${id}`);
const docs = readBundle(id);
await upsertChannels(docs);
await upsertPlaylistRow(adapter, groupCount(docs), { lastSync: 'Ships with TVApp2', status: 'good' });
logger.ok('seed', `[${id}] initialized ${docs.length} channels from bundle`);
return validateIntegrity(id);
}
/** Restore a source EXACTLY to its committed bundle (drops stale channels, then reinserts). */
export async function resetFromBundle(id) {
const adapter = getSource(id);
if (!adapter)
throw new Error(`unknown source: ${id}`);
const docs = readBundle(id);
await SourceChannel.deleteMany({ source: id });
await SourceChannel.insertMany(docs, { ordered: false });
await upsertPlaylistRow(adapter, groupCount(docs), { lastSync: 'Reset from bundle', status: 'good' });
logger.ok('seed', `[${id}] reset ${docs.length} channels from bundle`);
return validateIntegrity(id);
}
/** Live refresh: run the adapter's build pipeline and upsert; updates Playlist sync metadata. */
export async function syncLive(id) {
const adapter = getSource(id);
if (!adapter)
throw new Error(`unknown source: ${id}`);
const result = await buildSource(adapter);
await upsertChannels(result.docs);
await upsertPlaylistRow(adapter, groupCount(result.docs), {
lastSync: new Date().toISOString(),
status: result.live ? 'good' : 'warn',
});
logger.ok('seed', `[${id}] live sync upserted ${result.count} channels (${result.live ? 'live' : 'snapshot'})`);
const report = await validateIntegrity(id);
return { report, live: result.live, count: result.count };
}
/** Boot guard: seed from bundle only if the (Default) playlist is missing or empty. */
export async function ensureSeeded(id) {
const report = await validateIntegrity(id);
if (report.playlistExists && report.channelCount > 0) {
logger.info('seed', `[${id}] already seeded (${report.channelCount} channels) — skipping init`);
return report;
}
logger.info('seed', `[${id}] not seeded — initializing from bundle`);
return initFromBundle(id);
}
/**
* Run once at startup for every registered source: guarantee the bundle baseline synchronously, then
* kick a non-blocking live sync (Both mode). A failed/slow live sync must NEVER block or crash boot —
* the bundle baseline already satisfies init.
*/
export async function bootInitSources(opts = {}) {
const liveSync = opts.liveSync ?? true;
for (const adapter of SOURCES) {
try {
const report = await ensureSeeded(adapter.id);
if (!report.ok) {
logger.warn('seed', `[${adapter.id}] integrity issues after init: ${report.issues.join(', ')}`);
}
if (liveSync) {
void syncLive(adapter.id).catch((err) => logger.warn('seed', `[${adapter.id}] live sync failed (keeping bundle baseline): ${err.message}`));
}
}
catch (err) {
logger.error('seed', `[${adapter.id}] init failed: ${err.message}`);
}
}
}
//# sourceMappingURL=seed.js.map

1
server/dist/sources/seed.js.map vendored Normal file

File diff suppressed because one or more lines are too long

46
server/dist/sources/translate.js vendored Normal file
View File

@@ -0,0 +1,46 @@
// Translation layer: project a canonical SourceChannel doc into the legacy UI Channel shape the Vue
// screens consume. Fields with no source equivalent are returned as explicit null (never fabricated)
// per the agreed schema-reconciliation decision; logoUrl + streamEntryUrl + isPlayable are added so
// the SPA can render real logos and play through the proxy. The frontend derives the proxy path from
// (source, streamEntryUrl); it is intentionally not stored.
// Deterministic hue from a stable string → keeps a channel's fallback logo color stable across syncs.
function hueFromString(s) {
let h = 0;
for (let i = 0; i < s.length; i++)
h = (h * 31 + s.charCodeAt(i)) >>> 0;
return h % 360;
}
function logoColorFor(id) {
return `oklch(0.5 0.16 ${hueFromString(id)})`;
}
function initialsFor(name) {
const ini = name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0])
.join('')
.toUpperCase();
return ini || '?';
}
export function toUiChannel(doc) {
return {
id: doc._id,
tvg_name: doc.name,
group: doc.groupLabel,
channel: null,
tvg_id: null,
state: doc.isPlayable ? 'active' : 'disabled',
epg: null,
status: null,
res: null,
source: doc.source,
url: doc.streamEntryUrl,
logoColor: logoColorFor(doc._id),
initials: initialsFor(doc.name),
logoUrl: doc.logoUrl,
streamEntryUrl: doc.streamEntryUrl,
isPlayable: doc.isPlayable,
};
}
//# sourceMappingURL=translate.js.map

1
server/dist/sources/translate.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/sources/translate.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,qGAAqG;AACrG,oGAAoG;AACpG,qGAAqG;AACrG,4DAA4D;AAuB5D,sGAAsG;AACtG,SAAS,aAAa,CAAC,CAAS;IAC9B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,GAAG,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,kBAAkB,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC;AAChD,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,GAAG,GAAG,IAAI;SACb,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,OAAO,CAAC;SACf,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAChB,IAAI,CAAC,EAAE,CAAC;SACR,WAAW,EAAE,CAAC;IACjB,OAAO,GAAG,IAAI,GAAG,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAqB;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,GAAG;QACX,QAAQ,EAAE,GAAG,CAAC,IAAI;QAClB,KAAK,EAAE,GAAG,CAAC,UAAU;QACrB,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;QAC7C,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,IAAI;QACZ,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,GAAG,EAAE,GAAG,CAAC,cAAc;QACvB,SAAS,EAAE,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;QAChC,QAAQ,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;AACJ,CAAC"}

6
server/dist/sources/types.js vendored Normal file
View File

@@ -0,0 +1,6 @@
// The source-adapter contract, ported from d-combine (sources/<id>/adapter.mjs). One object per
// source captures ONLY what differs between sources; the generic core (buildSource, proxyHandler,
// playlist) consumes any adapter without per-source branching. Adding a source = one adapter +
// one registry line.
export {};
//# sourceMappingURL=types.js.map

1
server/dist/sources/types.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sources/types.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,kGAAkG;AAClG,+FAA+F;AAC/F,qBAAqB"}