mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-12 20:05:42 -04:00
initial push from external dev branches
This commit is contained in:
116
server/dist/sources/adapters/dulo.js
vendored
Normal file
116
server/dist/sources/adapters/dulo.js
vendored
Normal 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
|
||||
Reference in New Issue
Block a user