mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-12 01:55:42 -04:00
116 lines
4.3 KiB
JavaScript
116 lines
4.3 KiB
JavaScript
// 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
|