mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-11 23:05:41 -04:00
137 lines
6.0 KiB
JavaScript
137 lines
6.0 KiB
JavaScript
// 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
|