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

40
server/dist/config.js vendored Normal file
View File

@@ -0,0 +1,40 @@
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
const VALID_SCHEMES = /^mongodb(\+srv)?:\/\//;
function resolveConfigPath() {
const envPath = process.env.TVAPP2_CONFIG;
if (envPath) {
if (!existsSync(envPath)) {
throw new Error(`TVAPP2_CONFIG points to "${envPath}" but the file does not exist.`);
}
return envPath;
}
const localPath = resolve(process.cwd(), 'config.local.json');
if (existsSync(localPath))
return localPath;
throw new Error('No config file found. Set TVAPP2_CONFIG to the mounted config path, ' +
'or create ./config.local.json for development.');
}
export function loadConfig() {
const path = resolveConfigPath();
const raw = readFileSync(path, 'utf8');
let parsed;
try {
parsed = JSON.parse(raw);
}
catch (err) {
throw new Error(`Config file at "${path}" is not valid JSON: ${err.message}`);
}
if (!parsed || typeof parsed !== 'object') {
throw new Error(`Config file at "${path}" must be a JSON object.`);
}
const obj = parsed;
const mongoUri = obj.mongoUri;
if (typeof mongoUri !== 'string' || !VALID_SCHEMES.test(mongoUri)) {
throw new Error(`Config "mongoUri" must be a string starting with mongodb:// or mongodb+srv:// (got ${JSON.stringify(mongoUri)}).`);
}
const port = typeof obj.port === 'number' ? obj.port : 3000;
const logLevel = typeof obj.logLevel === 'string' ? obj.logLevel : 'info';
return { mongoUri, port, logLevel };
}
//# sourceMappingURL=config.js.map

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

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC1C,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,4BAA4B,OAAO,gCAAgC,CAAC,CAAC;QACvF,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAE5C,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,gDAAgD,CACnD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,wBAAyB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,0BAA0B,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,GAAG,GAAG,MAAiC,CAAC;IAE9C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC9B,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CACb,sFAAsF,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CACnH,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IAE1E,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACtC,CAAC"}

24
server/dist/db.js vendored Normal file
View File

@@ -0,0 +1,24 @@
import mongoose from 'mongoose';
export async function connect(uri) {
mongoose.connection.on('error', (err) => {
console.error('[mongo] connection error:', err.message);
});
mongoose.connection.on('disconnected', () => {
console.warn('[mongo] disconnected');
});
mongoose.connection.on('reconnected', () => {
console.info('[mongo] reconnected');
});
await mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000,
maxPoolSize: 10,
});
console.info('[mongo] connected');
}
export async function disconnect() {
await mongoose.disconnect();
}
export function isConnected() {
return mongoose.connection.readyState === 1;
}
//# sourceMappingURL=db.js.map

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

@@ -0,0 +1 @@
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAW;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACtC,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;QAC1C,OAAO,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,aAAa,EAAE,GAAG,EAAE;QACzC,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1B,wBAAwB,EAAE,IAAI;QAC9B,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,QAAQ,CAAC,UAAU,CAAC,UAAU,KAAK,CAAC,CAAC;AAC9C,CAAC"}

79
server/dist/index.js vendored Normal file
View File

@@ -0,0 +1,79 @@
import { existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import express from 'express';
import { loadConfig } from './config.js';
import { connect, disconnect } from './db.js';
import { healthRouter } from './routes/health.js';
import { playlistsRouter } from './routes/playlists.js';
import { epgSourcesRouter } from './routes/epgSources.js';
import { channelsRouter } from './routes/channels.js';
import { activeStreamsRouter } from './routes/activeStreams.js';
import { customPlaylistsRouter } from './routes/customPlaylists.js';
import { programsRouter } from './routes/programs.js';
import { activityRouter } from './routes/activity.js';
import { streamSessionsRouter } from './routes/streamSessions.js';
import { sourcesRouter } from './routes/sources.js';
import { bootInitSources } from './sources/seed.js';
async function main() {
const config = loadConfig();
try {
await connect(config.mongoUri);
}
catch (err) {
console.error('[startup] failed to connect to mongo:', err.message);
process.exit(1);
}
// Ingest the established (Default) source playlists: guarantee each from its committed bundle
// (idempotent), then kick a non-blocking live sync. Runs in both Docker variants via this single
// boot path. A failure here must not prevent the API from serving.
try {
await bootInitSources();
}
catch (err) {
console.error('[startup] source init error (continuing):', err.message);
}
const app = express();
app.use(express.json());
app.use('/api/health', healthRouter);
app.use('/api/playlists', playlistsRouter);
app.use('/api/epg-sources', epgSourcesRouter);
app.use('/api/channels', channelsRouter);
app.use('/api/active-streams', activeStreamsRouter);
app.use('/api/custom-playlists', customPlaylistsRouter);
app.use('/api/epg-programs', programsRouter);
app.use('/api/activity', activityRouter);
app.use('/api/stream-sessions', streamSessionsRouter);
// Generic source API (manifest, stream proxy, status, sync/reset) — mounted at root since its
// paths span /api/sources and /api/v1.
app.use(sourcesRouter);
const here = dirname(fileURLToPath(import.meta.url));
const publicDir = resolve(here, '..', 'public');
if (existsSync(publicDir)) {
app.use(express.static(publicDir));
app.get(/^\/(?!api\/).*/, (_req, res) => {
res.sendFile(resolve(publicDir, 'index.html'));
});
console.info(`[http] serving SPA from ${publicDir}`);
}
app.use((err, _req, res, _next) => {
console.error('[api] error:', err.message);
res.status(500).json({ error: 'internal_error' });
});
const server = app.listen(config.port, () => {
console.info(`[api] listening on :${config.port}`);
});
const shutdown = async (signal) => {
console.info(`[shutdown] received ${signal}`);
server.close();
await disconnect();
process.exit(0);
};
process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));
}
main().catch((err) => {
console.error('[startup] fatal:', err);
process.exit(1);
});
//# sourceMappingURL=index.js.map

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

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

25
server/dist/models/ActiveStream.js vendored Normal file
View File

@@ -0,0 +1,25 @@
import { Schema, model } from 'mongoose';
const ActiveStreamSchema = new Schema({
id: { type: String, required: true, unique: true, index: true },
channelId: { type: String, required: true, index: true },
status: { type: String, required: true },
uptime: { type: String, required: true },
uptimeMin: { type: Number, required: true },
viewers: { type: Number, required: true },
peakViewers: { type: Number, required: true },
bitrate: { type: Number, required: true },
targetBitrate: { type: Number, required: true },
codec: { type: String, required: true },
audio: { type: String, required: true },
container: { type: String, required: true },
resolution: { type: String, required: true },
fps: { type: Number, required: true },
sourceUrl: { type: String, required: true },
sourceHost: { type: String, required: true },
droppedFrames: { type: Number, required: true },
droppedRatio: { type: Number, required: true },
latency: { type: Number, required: true },
bandwidth: { type: Number, required: true },
}, { versionKey: false });
export const ActiveStream = model('ActiveStream', ActiveStreamSchema);
//# sourceMappingURL=ActiveStream.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ActiveStream.js","sourceRoot":"","sources":["../../src/models/ActiveStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,kBAAkB,GAAG,IAAI,MAAM,CACnC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC7C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC/C,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC/C,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC9C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC5C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC"}

9
server/dist/models/Activity.js vendored Normal file
View File

@@ -0,0 +1,9 @@
import { Schema, model } from 'mongoose';
const ActivitySchema = new Schema({
when: { type: String, required: true },
icon: { type: String, required: true },
html: { type: String, required: true },
order: { type: Number, required: true, index: true },
}, { versionKey: false });
export const Activity = model('Activity', ActivitySchema);
//# sourceMappingURL=Activity.js.map

1
server/dist/models/Activity.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"Activity.js","sourceRoot":"","sources":["../../src/models/Activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,IAAI,MAAM,CAC/B;IACE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC"}

18
server/dist/models/Channel.js vendored Normal file
View File

@@ -0,0 +1,18 @@
import { Schema, model } from 'mongoose';
const ChannelSchema = new Schema({
id: { type: String, required: true, unique: true, index: true },
tvg_name: { type: String, required: true },
group: { type: String, required: true },
channel: { type: Number, required: true },
tvg_id: { type: String, default: null },
state: { type: String, enum: ['active', 'disabled'], required: true },
epg: { type: String, enum: ['matched', 'unmatched'], required: true },
source: { type: String, required: true },
url: { type: String, required: true, unique: true },
status: { type: String, required: true },
res: { type: String, required: true },
logoColor: { type: String, required: true },
initials: { type: String, required: true },
}, { versionKey: false });
export const Channel = model('Channel', ChannelSchema);
//# sourceMappingURL=Channel.js.map

1
server/dist/models/Channel.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../src/models/Channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,aAAa,GAAG,IAAI,MAAM,CAC9B;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACvC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrE,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACnD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC3C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC"}

10
server/dist/models/CustomPlaylist.js vendored Normal file
View File

@@ -0,0 +1,10 @@
import { Schema, model } from 'mongoose';
const CustomPlaylistSchema = new Schema({
id: { type: String, required: true, unique: true, index: true },
name: { type: String, required: true },
slug: { type: String, required: true, index: true },
channels: { type: Number, required: true },
updated: { type: String, required: true },
}, { versionKey: false });
export const CustomPlaylist = model('CustomPlaylist', CustomPlaylistSchema);
//# sourceMappingURL=CustomPlaylist.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CustomPlaylist.js","sourceRoot":"","sources":["../../src/models/CustomPlaylist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,oBAAoB,GAAG,IAAI,MAAM,CACrC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACnD,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC1C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,CAAC"}

15
server/dist/models/EpgSource.js vendored Normal file
View File

@@ -0,0 +1,15 @@
import { Schema, model } from 'mongoose';
const EpgSourceSchema = new Schema({
id: { type: String, required: true, unique: true, index: true },
name: { type: String, required: true },
url: { type: String, required: true },
channels: { type: Number, required: true },
programs: { type: Number, required: true },
lastSync: { type: String, required: true },
status: { type: String, required: true },
auto: { type: Boolean, required: true },
interval: { type: String, required: true },
builtin: { type: Boolean },
}, { versionKey: false });
export const EpgSource = model('EpgSource', EpgSourceSchema);
//# sourceMappingURL=EpgSource.js.map

1
server/dist/models/EpgSource.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"EpgSource.js","sourceRoot":"","sources":["../../src/models/EpgSource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,eAAe,GAAG,IAAI,MAAM,CAChC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;CAC3B,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC"}

18
server/dist/models/Playlist.js vendored Normal file
View File

@@ -0,0 +1,18 @@
import { Schema, model } from 'mongoose';
const PlaylistSchema = new Schema({
id: { type: String, required: true, unique: true, index: true },
name: { type: String, required: true },
url: { type: String, required: true },
groups: { type: Number, required: true },
lastSync: { type: String, required: true },
status: { type: String, required: true },
auto: { type: Boolean, required: true },
interval: { type: String, required: true },
builtin: { type: Boolean },
// Set for the established (Default) source playlists (dulo/common/dlhd). When present, the
// playlist's channels live in the SourceChannel collection (queried by this `source`) instead
// of the legacy PlaylistChannel join. Unset for legacy/mock playlists.
source: { type: String, default: null, index: true },
}, { versionKey: false });
export const Playlist = model('Playlist', PlaylistSchema);
//# sourceMappingURL=Playlist.js.map

1
server/dist/models/Playlist.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"Playlist.js","sourceRoot":"","sources":["../../src/models/Playlist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,IAAI,MAAM,CAC/B;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC1B,2FAA2F;IAC3F,8FAA8F;IAC9F,uEAAuE;IACvE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC"}

10
server/dist/models/PlaylistChannel.js vendored Normal file
View File

@@ -0,0 +1,10 @@
import { Schema, model } from 'mongoose';
const PlaylistChannelSchema = new Schema({
playlistId: { type: String, required: true, index: true },
channelId: { type: String, required: true, index: true },
order: { type: Number, required: true },
}, { versionKey: false });
PlaylistChannelSchema.index({ playlistId: 1, channelId: 1 }, { unique: true });
PlaylistChannelSchema.index({ playlistId: 1, order: 1 });
export const PlaylistChannel = model('PlaylistChannel', PlaylistChannelSchema);
//# sourceMappingURL=PlaylistChannel.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PlaylistChannel.js","sourceRoot":"","sources":["../../src/models/PlaylistChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,qBAAqB,GAAG,IAAI,MAAM,CACtC;IACE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACzD,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CACxC,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,qBAAqB,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/E,qBAAqB,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC"}

11
server/dist/models/Program.js vendored Normal file
View File

@@ -0,0 +1,11 @@
import { Schema, model } from 'mongoose';
const ProgramSchema = new Schema({
channelId: { type: String, required: true, index: true },
start: { type: Number, required: true },
end: { type: Number, required: true },
title: { type: String, required: true },
cat: { type: String, required: true },
}, { versionKey: false });
ProgramSchema.index({ channelId: 1, start: 1 });
export const Program = model('Program', ProgramSchema);
//# sourceMappingURL=Program.js.map

1
server/dist/models/Program.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"Program.js","sourceRoot":"","sources":["../../src/models/Program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,aAAa,GAAG,IAAI,MAAM,CAC9B;IACE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACxD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CACtC,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,aAAa,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAEhD,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC"}

22
server/dist/models/SourceChannel.js vendored Normal file
View File

@@ -0,0 +1,22 @@
import { Schema, model } from 'mongoose';
const SourceChannelSchema = new Schema({
_id: { type: String, required: true },
source: { type: String, required: true },
sourceChannelId: { type: String, required: true },
name: { type: String, required: true },
category: { type: String, default: null },
groupKey: { type: String, required: true },
groupLabel: { type: String, required: true },
logoUrl: { type: String, default: null },
streamEntryUrl: { type: String, required: true },
isPlayable: { type: Boolean, required: true },
sourceCreatedAt: { type: String, default: null },
sourceUpdatedAt: { type: String, default: null },
ingestedAt: { type: String, required: true },
}, { versionKey: false });
// Covers the per-source grouped/ordered listing query (source → groupKey → name).
SourceChannelSchema.index({ source: 1, groupKey: 1, name: 1 });
// Dead-channel / playable filtering per source.
SourceChannelSchema.index({ source: 1, isPlayable: 1 });
export const SourceChannel = model('SourceChannel', SourceChannelSchema);
//# sourceMappingURL=SourceChannel.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SourceChannel.js","sourceRoot":"","sources":["../../src/models/SourceChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAwBzC,MAAM,mBAAmB,GAAG,IAAI,MAAM,CACpC;IACE,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACjD,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACzC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACxC,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAChD,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC7C,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IAChD,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IAChD,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC7C,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,kFAAkF;AAClF,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;AAC/D,gDAAgD;AAChD,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;AAExD,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAmB,eAAe,EAAE,mBAAmB,CAAC,CAAC"}

11
server/dist/models/StreamSession.js vendored Normal file
View File

@@ -0,0 +1,11 @@
import { Schema, model } from 'mongoose';
const StreamSessionSchema = new Schema({
ip: { type: String, required: true },
region: { type: String, required: true },
client: { type: String, required: true },
joined: { type: String, required: true },
bitrate: { type: String, required: true },
order: { type: Number, required: true, index: true },
}, { versionKey: false });
export const StreamSession = model('StreamSession', StreamSessionSchema);
//# sourceMappingURL=StreamSession.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"StreamSession.js","sourceRoot":"","sources":["../../src/models/StreamSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,mBAAmB,GAAG,IAAI,MAAM,CACpC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACpC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;CACrD,EACD,EAAE,UAAU,EAAE,KAAK,EAAE,CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC"}

13
server/dist/routes/activeStreams.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { ActiveStream } from '../models/ActiveStream.js';
export const activeStreamsRouter = Router();
activeStreamsRouter.get('/', async (_req, res, next) => {
try {
const docs = await ActiveStream.find({}, { _id: 0 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=activeStreams.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"activeStreams.js","sourceRoot":"","sources":["../../src/routes/activeStreams.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,EAAE,CAAC;AAE5C,mBAAmB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACrD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

13
server/dist/routes/activity.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { Activity } from '../models/Activity.js';
export const activityRouter = Router();
activityRouter.get('/', async (_req, res, next) => {
try {
const docs = await Activity.find({}, { _id: 0, order: 0 }).sort({ order: 1 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=activity.js.map

1
server/dist/routes/activity.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/routes/activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrF,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

22
server/dist/routes/channels.js vendored Normal file
View File

@@ -0,0 +1,22 @@
import { Router } from 'express';
import { Channel } from '../models/Channel.js';
import { SourceChannel } from '../models/SourceChannel.js';
export const channelsRouter = Router();
// GET /api/channels → legacy mock channels (drives the existing dashboard bootstrap)
// GET /api/channels?source=<id> → canonical normalized SourceChannel docs for a (Default) source
// playlist (the d-combine "path forward" contract, served over Mongo)
channelsRouter.get('/', async (req, res, next) => {
try {
const source = typeof req.query.source === 'string' ? req.query.source : null;
if (source) {
const docs = await SourceChannel.find({ source }).sort({ groupKey: 1, name: 1 }).lean();
return res.json(docs);
}
const docs = await Channel.find({}, { _id: 0 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=channels.js.map

1
server/dist/routes/channels.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/routes/channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,gGAAgG;AAChG,iGAAiG;AACjG,sGAAsG;AACtG,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9E,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACxF,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

13
server/dist/routes/customPlaylists.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { CustomPlaylist } from '../models/CustomPlaylist.js';
export const customPlaylistsRouter = Router();
customPlaylistsRouter.get('/', async (_req, res, next) => {
try {
const docs = await CustomPlaylist.find({}, { _id: 0 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=customPlaylists.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"customPlaylists.js","sourceRoot":"","sources":["../../src/routes/customPlaylists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,EAAE,CAAC;AAE9C,qBAAqB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

13
server/dist/routes/epgSources.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { EpgSource } from '../models/EpgSource.js';
export const epgSourcesRouter = Router();
epgSourcesRouter.get('/', async (_req, res, next) => {
try {
const docs = await EpgSource.find({}, { _id: 0 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=epgSources.js.map

1
server/dist/routes/epgSources.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"epgSources.js","sourceRoot":"","sources":["../../src/routes/epgSources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,EAAE,CAAC;AAEzC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAClD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACzD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

7
server/dist/routes/health.js vendored Normal file
View File

@@ -0,0 +1,7 @@
import { Router } from 'express';
import { isConnected } from '../db.js';
export const healthRouter = Router();
healthRouter.get('/', (_req, res) => {
res.json({ ok: true, mongo: isConnected() ? 'connected' : 'disconnected' });
});
//# sourceMappingURL=health.js.map

1
server/dist/routes/health.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC;AAErC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAClC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;AAC9E,CAAC,CAAC,CAAC"}

106
server/dist/routes/playlists.js vendored Normal file
View File

@@ -0,0 +1,106 @@
import { Router } from 'express';
import { Playlist } from '../models/Playlist.js';
import { PlaylistChannel } from '../models/PlaylistChannel.js';
import { Channel } from '../models/Channel.js';
import { SourceChannel } from '../models/SourceChannel.js';
import { toUiChannel } from '../sources/translate.js';
export const playlistsRouter = Router();
// Channel count comes from SourceChannel for (Default) source playlists, else the legacy join table.
async function channelCountFor(doc) {
if (doc.source)
return SourceChannel.countDocuments({ source: doc.source });
return PlaylistChannel.countDocuments({ playlistId: doc.id });
}
playlistsRouter.get('/', async (_req, res, next) => {
try {
const docs = await Playlist.find({}, { _id: 0 }).lean();
const [legacyCounts, sourceCounts] = await Promise.all([
PlaylistChannel.aggregate([
{ $group: { _id: '$playlistId', count: { $sum: 1 } } },
]),
SourceChannel.aggregate([
{ $group: { _id: '$source', count: { $sum: 1 } } },
]),
]);
const legacyById = new Map(legacyCounts.map((c) => [c._id, c.count]));
const sourceBySource = new Map(sourceCounts.map((c) => [c._id, c.count]));
res.json(docs.map((d) => ({
...d,
channels: d.source ? sourceBySource.get(d.source) ?? 0 : legacyById.get(d.id) ?? 0,
})));
}
catch (err) {
next(err);
}
});
playlistsRouter.get('/:id', async (req, res, next) => {
try {
const doc = await Playlist.findOne({ id: req.params.id }, { _id: 0 }).lean();
if (!doc)
return res.status(404).json({ error: 'not_found' });
res.json({ ...doc, channels: await channelCountFor(doc) });
}
catch (err) {
next(err);
}
});
// List channels in a playlist. (Default) source playlists project the canonical SourceChannel docs
// through the translation layer (UI shape, nulls for unmapped fields); legacy playlists use the join.
playlistsRouter.get('/:id/channels', async (req, res, next) => {
try {
const playlist = await Playlist.findOne({ id: req.params.id }).lean();
if (!playlist)
return res.status(404).json({ error: 'not_found' });
if (playlist.source) {
const docs = await SourceChannel.find({ source: playlist.source })
.sort({ groupKey: 1, name: 1 })
.lean();
return res.json(docs.map((d, order) => ({ ...toUiChannel(d), order })));
}
const memberships = await PlaylistChannel.find({ playlistId: req.params.id }, { _id: 0 })
.sort({ order: 1 })
.lean();
const channelIds = memberships.map((m) => m.channelId);
const channels = await Channel.find({ id: { $in: channelIds } }, { _id: 0 }).lean();
const byId = new Map(channels.map((c) => [c.id, c]));
res.json(memberships
.map((m) => {
const ch = byId.get(m.channelId);
return ch ? { ...ch, order: m.order } : null;
})
.filter(Boolean));
}
catch (err) {
next(err);
}
});
// Add a channel to a (legacy) playlist (idempotent on the unique pair).
playlistsRouter.post('/:id/channels', async (req, res, next) => {
try {
const { channelId, order } = req.body ?? {};
if (typeof channelId !== 'string' || typeof order !== 'number') {
return res.status(400).json({ error: 'channelId (string) and order (number) required' });
}
const doc = await PlaylistChannel.findOneAndUpdate({ playlistId: req.params.id, channelId }, { $set: { order } }, { upsert: true, new: true, projection: { _id: 0 } }).lean();
res.status(201).json(doc);
}
catch (err) {
next(err);
}
});
// Remove a channel from a (legacy) playlist.
playlistsRouter.delete('/:id/channels/:channelId', async (req, res, next) => {
try {
const result = await PlaylistChannel.deleteOne({
playlistId: req.params.id,
channelId: req.params.channelId,
});
if (result.deletedCount === 0)
return res.status(404).json({ error: 'not_found' });
res.status(204).end();
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=playlists.js.map

1
server/dist/routes/playlists.js.map vendored Normal file

File diff suppressed because one or more lines are too long

19
server/dist/routes/programs.js vendored Normal file
View File

@@ -0,0 +1,19 @@
import { Router } from 'express';
import { Program } from '../models/Program.js';
export const programsRouter = Router();
// All programs, grouped by channelId — matches the EPG_PROGRAMS shape the SPA expects.
programsRouter.get('/', async (_req, res, next) => {
try {
const docs = await Program.find({}, { _id: 0 }).sort({ channelId: 1, start: 1 }).lean();
const grouped = {};
for (const d of docs) {
const list = grouped[d.channelId] ?? (grouped[d.channelId] = []);
list.push({ start: d.start, end: d.end, title: d.title, cat: d.cat });
}
res.json(grouped);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=programs.js.map

1
server/dist/routes/programs.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"programs.js","sourceRoot":"","sources":["../../src/routes/programs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC;AAEvC,uFAAuF;AACvF,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxF,MAAM,OAAO,GAAsF,EAAE,CAAC;QACtG,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACxE,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

88
server/dist/routes/sources.js vendored Normal file
View File

@@ -0,0 +1,88 @@
// Generic source RestAPI, ported from ../d-combine/server.mjs into TVApp2's Express stack. One router
// serves every source by iterating the registry — adding a source needs zero route changes.
//
// GET /api/sources manifest (drives the SPA; one entry per registered source)
// GET /api/sources/:id/status runtime provenance (dlhd: live mirror; null otherwise)
// GET /api/sources/:id/metrics per-source proxy counters
// POST /api/sources/:id/sync live refresh → upsert channels + Playlist sync metadata
// POST /api/sources/:id/reset restore the committed bundle baseline
// GET /api/v1/:source/* single stream proxy; the :source segment binds that source's
// resolve+proxy behavior (createProxyHandler per adapter)
//
// Mounted at the app root (app.use(sourcesRouter)) because its paths span /api/sources, /api/v1, …
import { Router } from 'express';
import { SOURCES, getSource } from '../sources/registry.js';
import { createProxyHandler } from '../sources/core/proxyHandler.js';
import { createMetrics, snapshotOne } from '../sources/core/metrics.js';
import { syncLive, resetFromBundle } from '../sources/seed.js';
export const sourcesRouter = Router();
// Build one proxy handler (+ metrics bag) per source once, then dispatch by the :source segment.
const metricsById = new Map();
const proxyHandlers = new Map();
for (const adapter of SOURCES) {
const m = createMetrics();
metricsById.set(adapter.id, m);
proxyHandlers.set(adapter.id, createProxyHandler(adapter, m));
}
// ── Manifest ────────────────────────────────────────────────────────────────
sourcesRouter.get('/api/sources', (_req, res) => {
res.json(SOURCES.map((s) => ({
id: s.id,
label: s.label,
grouping: s.grouping,
sourceUrl: `/api/channels?source=${s.id}`, // normalized catalog over Mongo
proxyPrefix: `/api/v1/${s.id}/`,
statusUrl: s.status ? `/api/sources/${s.id}/status` : null,
})));
});
// ── Per-source runtime status (dlhd mirror provenance; null for sources without one) ──
sourcesRouter.get('/api/sources/:id/status', async (req, res, next) => {
try {
const adapter = getSource(req.params.id);
if (!adapter)
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
const status = adapter.status ? await adapter.status() : null;
res.json(status ?? null);
}
catch (err) {
next(err);
}
});
// ── Per-source proxy metrics ──────────────────────────────────────────────────
sourcesRouter.get('/api/sources/:id/metrics', (req, res) => {
const m = metricsById.get(req.params.id);
if (!m)
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
res.json(snapshotOne(m));
});
// ── Live sync (refresh channels + Playlist sync metadata from upstream) ───────
sourcesRouter.post('/api/sources/:id/sync', async (req, res, next) => {
try {
if (!getSource(req.params.id))
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
res.json(await syncLive(req.params.id));
}
catch (err) {
next(err);
}
});
// ── Reset to the committed bundle baseline ────────────────────────────────────
sourcesRouter.post('/api/sources/:id/reset', async (req, res, next) => {
try {
if (!getSource(req.params.id))
return res.status(404).json({ error: `Unknown source: ${req.params.id}` });
res.json(await resetFromBundle(req.params.id));
}
catch (err) {
next(err);
}
});
// ── Single stream proxy API ───────────────────────────────────────────────────
sourcesRouter.get('/api/v1/:source/*', (req, res) => {
const handler = proxyHandlers.get(req.params.source);
if (!handler) {
return res.status(404).type('text/plain').send(`Unknown source: ${req.params.source}`);
}
return handler(req, res, () => undefined);
});
//# sourceMappingURL=sources.js.map

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

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

13
server/dist/routes/streamSessions.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { StreamSession } from '../models/StreamSession.js';
export const streamSessionsRouter = Router();
streamSessionsRouter.get('/', async (_req, res, next) => {
try {
const docs = await StreamSession.find({}, { _id: 0, order: 0 }).sort({ order: 1 }).lean();
res.json(docs);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=streamSessions.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"streamSessions.js","sourceRoot":"","sources":["../../src/routes/streamSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,EAAE,CAAC;AAE7C,oBAAoB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC"}

152
server/dist/seed.js vendored Normal file
View File

@@ -0,0 +1,152 @@
// Seed script — populates every collection from inlined mock data.
// Idempotent: drops and refills each collection.
//
// cd server && npm run seed
//
// Uses the same config resolution as the API (TVAPP2_CONFIG or ./config.local.json).
import { loadConfig } from './config.js';
import { connect, disconnect } from './db.js';
import { Playlist } from './models/Playlist.js';
import { Channel } from './models/Channel.js';
import { PlaylistChannel } from './models/PlaylistChannel.js';
import { EpgSource } from './models/EpgSource.js';
import { CustomPlaylist } from './models/CustomPlaylist.js';
import { ActiveStream } from './models/ActiveStream.js';
import { Program } from './models/Program.js';
import { Activity } from './models/Activity.js';
import { StreamSession } from './models/StreamSession.js';
const PLAYLISTS = [
{ id: 'pl-default', name: 'Default', url: 'bundled://tvapp2/default.m3u', groups: 6, lastSync: 'Ships with TVApp2', status: 'good', auto: true, interval: 'Auto-updated', builtin: true },
{ id: 'pl-iptv-pro', name: 'IPTV-Pro Main', url: 'https://iptv-pro.example.com/playlist.m3u8', groups: 8, lastSync: '2 minutes ago', status: 'good', auto: true, interval: 'Every 6 hours' },
{ id: 'pl-free-uk', name: 'Free UK Bouquet', url: 'https://iptv-org.github.io/iptv/countries/uk.m3u', groups: 4, lastSync: '1 hour ago', status: 'good', auto: true, interval: 'Daily' },
{ id: 'pl-archive', name: 'Archive (legacy)', url: 'file:///playlists/archive-2023.m3u', groups: 3, lastSync: '3 days ago', status: 'warn', auto: false, interval: 'Manual' },
];
const EPG_SOURCES = [
{ id: 'epg-default', name: 'Default', url: 'bundled://tvapp2/default.xml.gz', channels: 86, programs: 5240, lastSync: 'Ships with TVApp2', status: 'good', auto: true, interval: 'Auto-updated', builtin: true },
{ id: 'epg-xmltv-uk', name: 'XMLTV UK Guide', url: 'https://epg.example.com/uk.xml.gz', channels: 124, programs: 8420, lastSync: '12 minutes ago', status: 'good', auto: true, interval: 'Every 12 hours' },
{ id: 'epg-iptv-org', name: 'iptv-org world EPG', url: 'https://iptv-org.github.io/epg/guides/uk/openepg.xml', channels: 412, programs: 24180, lastSync: '2 hours ago', status: 'good', auto: true, interval: 'Daily' },
];
const CHANNEL_SEEDS = [
{ tvg_name: 'BBC One HD', group: 'Entertainment', channel: 101, tvg_id: 'bbc.one.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'BBC Two HD', group: 'Entertainment', channel: 102, tvg_id: 'bbc.two.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'BBC News', group: 'News', channel: 231, tvg_id: 'bbc.news.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Sky Sports Main', group: 'Sport', channel: 401, tvg_id: 'sky.sports.main.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'Sky Sports F1', group: 'Sport', channel: 406, tvg_id: 'sky.sports.f1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'ITV1 HD', group: 'Entertainment', channel: 103, tvg_id: 'itv1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'Channel 4 HD', group: 'Entertainment', channel: 104, tvg_id: 'channel4.uk', state: 'active', epg: 'matched', status: 'warn', res: '1080p' },
{ tvg_name: 'Film4', group: 'Movies', channel: 315, tvg_id: 'film4.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Discovery Channel', group: 'Documentary', channel: 520, tvg_id: 'discovery.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'National Geographic', group: 'Documentary', channel: 521, tvg_id: 'natgeo.uk', state: 'active', epg: 'unmatched', status: 'good', res: '1080p' },
{ tvg_name: 'CNN International', group: 'News', channel: 233, tvg_id: 'cnn.int', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Al Jazeera English', group: 'News', channel: 235, tvg_id: 'aljazeera.en', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Cartoon Network', group: 'Kids', channel: 601, tvg_id: 'cartoonnet.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Nick Jr.', group: 'Kids', channel: 615, tvg_id: null, state: 'disabled', epg: 'unmatched', status: 'warn', res: '720p' },
{ tvg_name: 'MTV Hits', group: 'Music', channel: 365, tvg_id: 'mtv.hits.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Kerrang!', group: 'Music', channel: 369, tvg_id: 'kerrang.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'Food Network', group: 'Lifestyle', channel: 240, tvg_id: 'foodnet.uk', state: 'active', epg: 'matched', status: 'good', res: '720p' },
{ tvg_name: 'HGTV', group: 'Lifestyle', channel: 242, tvg_id: null, state: 'disabled', epg: 'unmatched', status: 'bad', res: '720p' },
{ tvg_name: 'TCM Movies', group: 'Movies', channel: 320, tvg_id: 'tcm.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
{ tvg_name: 'Eurosport 1', group: 'Sport', channel: 410, tvg_id: 'eurosport1.uk', state: 'active', epg: 'matched', status: 'good', res: '1080p' },
];
const CHANNELS = CHANNEL_SEEDS.map((c, i) => ({
id: `ch-${i}`,
...c,
source: 'Default',
url: `http://sample.stream.com/channel/1.m3u8?ch=ch-${i}`,
logoColor: `oklch(0.5 0.16 ${(i * 47) % 360})`,
initials: c.tvg_name.split(/\s+/).slice(0, 2).map((w) => w[0]).join('').toUpperCase(),
}));
const CUSTOM_PLAYLISTS = [
{ id: 'cust-sports-night', name: 'Sports Night', slug: 'sports-night', channels: 12, updated: '2 days ago' },
{ id: 'cust-kids-safe', name: 'Kids Safe', slug: 'kids-safe', channels: 8, updated: 'yesterday' },
{ id: 'cust-news-rotation', name: 'News Rotation', slug: 'news-rotation', channels: 6, updated: '5 hours ago' },
{ id: 'cust-living-room', name: 'Living Room Favorites', slug: 'living-room', channels: 18, updated: '1 week ago' },
];
const ACTIVE_STREAMS = [
{ id: 'as-1', channelId: 'ch-0', status: 'good', uptime: '4h 12m', uptimeMin: 252, viewers: 142, peakViewers: 168, bitrate: 6.4, targetBitrate: 6.0, codec: 'H.264 High@4.1', audio: 'AAC LC 2.0 · 128k', container: 'HLS / TS', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/bbc-one/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 0, droppedRatio: 0.00, latency: 2.1, bandwidth: 912 },
{ id: 'as-2', channelId: 'ch-3', status: 'good', uptime: '1h 48m', uptimeMin: 108, viewers: 89, peakViewers: 112, bitrate: 8.2, targetBitrate: 8.0, codec: 'H.264 High@4.2', audio: 'AC3 5.1 · 384k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/sky-sports-main/index.m3u8', sourceHost: 'edge-lon-02', droppedFrames: 14, droppedRatio: 0.01, latency: 1.8, bandwidth: 730 },
{ id: 'as-3', channelId: 'ch-2', status: 'good', uptime: '22h 06m', uptimeMin: 1326, viewers: 47, peakViewers: 61, bitrate: 3.1, targetBitrate: 3.0, codec: 'H.264 Main@3.1', audio: 'AAC LC 2.0 · 96k', container: 'HLS / TS', resolution: '1280×720', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/bbc-news/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 2, droppedRatio: 0.00, latency: 2.4, bandwidth: 145 },
{ id: 'as-4', channelId: 'ch-6', status: 'warn', uptime: '12m', uptimeMin: 12, viewers: 8, peakViewers: 8, bitrate: 4.1, targetBitrate: 5.0, codec: 'H.264 High@4.0', audio: 'AAC LC 2.0 · 128k', container: 'HLS / TS', resolution: '1920×1080', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/channel4/index.m3u8', sourceHost: 'edge-ams-01', droppedFrames: 184, droppedRatio: 0.47, latency: 4.7, bandwidth: 34 },
{ id: 'as-5', channelId: 'ch-4', status: 'good', uptime: '3h 41m', uptimeMin: 221, viewers: 31, peakViewers: 44, bitrate: 7.9, targetBitrate: 8.0, codec: 'H.264 High@4.2', audio: 'AC3 5.1 · 384k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 50, sourceUrl: 'http://stream.iptv-pro.example.com/live/sky-sports-f1/index.m3u8', sourceHost: 'edge-lon-02', droppedFrames: 0, droppedRatio: 0.00, latency: 1.9, bandwidth: 245 },
{ id: 'as-6', channelId: 'ch-8', status: 'good', uptime: '45m', uptimeMin: 45, viewers: 12, peakViewers: 12, bitrate: 5.6, targetBitrate: 6.0, codec: 'H.265 Main@4.0', audio: 'AAC LC 2.0 · 128k', container: 'HLS / fMP4', resolution: '1920×1080', fps: 25, sourceUrl: 'http://stream.iptv-pro.example.com/live/discovery/index.m3u8', sourceHost: 'edge-fra-04', droppedFrames: 1, droppedRatio: 0.00, latency: 2.2, bandwidth: 68 },
{ id: 'as-7', channelId: 'ch-17', status: 'bad', uptime: '—', uptimeMin: 0, viewers: 0, peakViewers: 4, bitrate: 0, targetBitrate: 5.0, codec: '—', audio: '—', container: 'HLS / TS', resolution: '—', fps: 0, sourceUrl: 'http://stream.iptv-pro.example.com/live/hgtv/index.m3u8', sourceHost: 'edge-ams-01', droppedFrames: 0, droppedRatio: 0, latency: 0, bandwidth: 0 },
];
const ACTIVITY = [
{ when: '2m', icon: 'sync', html: '<b>IPTV-Pro Main</b> synced — 142 channels, no changes' },
{ when: '12m', icon: 'epg', html: '<b>XMLTV UK Guide</b> imported — 8,420 programs across 124 channels' },
{ when: '1h', icon: 'map', html: 'Manual mapping: <b>HGTV</b> → <code class="mono">hgtv.uk</code>' },
{ when: '1h', icon: 'warn', html: '<b>Free UK Bouquet</b> reports 3 channels offline (HTTP 503)' },
{ when: '3h', icon: 'edit', html: 'Renamed <b>Discovery</b> → <b>Discovery Channel</b>' },
{ when: 'Yest.', icon: 'add', html: 'Playlist <b>IPTV-Pro Main</b> added (142 channels)' },
];
const STREAM_SESSIONS = [
{ ip: '82.14.221.47', region: 'GB · London', client: 'VLC / Linux', joined: '2m ago', bitrate: '6.4 Mbps' },
{ ip: '192.81.45.12', region: 'DE · Frankfurt', client: 'Tivimate / Android TV', joined: '8m ago', bitrate: '6.4 Mbps' },
{ ip: '104.18.92.5', region: 'NL · Amsterdam', client: 'OTT Navigator / FireTV', joined: '14m ago', bitrate: '3.1 Mbps' },
{ ip: '176.58.103.9', region: 'GB · Manchester', client: 'Kodi 21', joined: '31m ago', bitrate: '6.4 Mbps' },
{ ip: '78.143.211.4', region: 'FR · Paris', client: 'IPTV Smarters / iOS', joined: '1h ago', bitrate: '3.1 Mbps' },
{ ip: '10.0.4.118', region: 'Local · LAN', client: 'ffmpeg / probe', joined: '3h ago', bitrate: '6.4 Mbps' },
];
const PROGRAM_LIBRARY = [
['Morning News', 'Live'], ['Breakfast Show', 'Lifestyle'], ['Market Report', 'Business'],
['Sports Roundup', 'Highlights'], ['Drama Hour', 'Series'], ['World Headlines', 'News'],
['Wildlife Special', 'Documentary'], ['Cooking with Anna', 'Lifestyle'],
['Classic Movies', 'Film'], ['Talk of the Day', 'Discussion'], ["Children's Hour", 'Kids'],
['Weather Watch', 'Weather'], ['Live Match', 'Football'], ['Tech Today', 'Technology'],
['Late Show', 'Comedy'], ['Documentary', 'Feature'], ['Music Mix', 'Music'],
['Quiz Night', 'Game show'], ['Reality TV', 'Series'], ['The Daily Brief', 'News'],
];
function rngFor(seed) {
let s = seed;
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
function generatePrograms(channelId, seedBase) {
const rng = rngFor(seedBase);
const progs = [];
let t = 0;
while (t < 24) {
const dur = [0.5, 1, 1, 1.5, 2][Math.floor(rng() * 5)];
const idx = Math.floor(rng() * PROGRAM_LIBRARY.length);
progs.push({ channelId, start: t, end: Math.min(24, t + dur), title: PROGRAM_LIBRARY[idx][0], cat: PROGRAM_LIBRARY[idx][1] });
t += dur;
}
return progs;
}
async function main() {
const config = loadConfig();
await connect(config.mongoUri);
await Promise.all([
Playlist.deleteMany({}),
Channel.deleteMany({}),
PlaylistChannel.deleteMany({}),
EpgSource.deleteMany({}),
CustomPlaylist.deleteMany({}),
ActiveStream.deleteMany({}),
Program.deleteMany({}),
Activity.deleteMany({}),
StreamSession.deleteMany({}),
]);
await Playlist.insertMany(PLAYLISTS);
await Channel.insertMany(CHANNELS);
await EpgSource.insertMany(EPG_SOURCES);
await CustomPlaylist.insertMany(CUSTOM_PLAYLISTS);
await ActiveStream.insertMany(ACTIVE_STREAMS);
// Default playlist holds every channel in order.
const defaultPlaylist = PLAYLISTS.find((p) => p.builtin) ?? PLAYLISTS[0];
await PlaylistChannel.insertMany(CHANNELS.map((c, order) => ({ playlistId: defaultPlaylist.id, channelId: c.id, order })));
// EPG programs for the first 12 channels (matches the original mock).
const programs = CHANNELS.slice(0, 12).flatMap((c, i) => generatePrograms(c.id, 100 + i * 7));
await Program.insertMany(programs);
await Activity.insertMany(ACTIVITY.map((a, order) => ({ ...a, order })));
await StreamSession.insertMany(STREAM_SESSIONS.map((s, order) => ({ ...s, order })));
console.info(`[seed] playlists=${PLAYLISTS.length} channels=${CHANNELS.length} ` +
`epg-sources=${EPG_SOURCES.length} custom-playlists=${CUSTOM_PLAYLISTS.length} ` +
`active-streams=${ACTIVE_STREAMS.length} programs=${programs.length} ` +
`activity=${ACTIVITY.length} stream-sessions=${STREAM_SESSIONS.length}`);
await disconnect();
}
main().catch((err) => {
console.error('[seed] failed:', err);
process.exit(1);
});
//# sourceMappingURL=seed.js.map

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

File diff suppressed because one or more lines are too long

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

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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