mirror of
https://gitlab.com/vyla-entertainment/TV.git
synced 2026-11-09 17:47:20 -05:00
112 lines
6.1 KiB
JavaScript
112 lines
6.1 KiB
JavaScript
(() => {
|
|
const cap = window.Capacitor || {};
|
|
const N = (cap.Plugins && cap.Plugins.VylaNative) || cap.registerPlugin('VylaNative');
|
|
window.API_BASE = 'http://127.0.0.1:38417';
|
|
|
|
const webFetch = window.fetch.bind(window);
|
|
const dec = new TextDecoder('utf-8');
|
|
const bytesOf = s => { const b = atob(s), u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i); return u };
|
|
const abortErr = () => new DOMException('The operation was aborted', 'AbortError');
|
|
|
|
const wrap = r => {
|
|
const bytes = bytesOf(r.body || ''), h = r.headers || {};
|
|
const text = async () => dec.decode(bytes);
|
|
return {
|
|
ok: r.status >= 200 && r.status < 300,
|
|
status: r.status,
|
|
statusText: '',
|
|
url: r.url,
|
|
headers: { get: k => { const v = h[String(k).toLowerCase()]; return v === undefined ? null : v }, has: k => String(k).toLowerCase() in h },
|
|
body: { getReader: () => { let d = false; return { read: async () => d ? { done: true, value: undefined } : (d = true, { done: false, value: bytes }) } }, cancel: async () => { } },
|
|
text,
|
|
json: async () => JSON.parse(await text()),
|
|
arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
};
|
|
};
|
|
|
|
window.fetch = (input, init = {}) => {
|
|
const isReq = typeof input !== 'string';
|
|
const url = (isReq ? (input && input.url) : input) || '';
|
|
if (!/^https?:\/\//i.test(url) || url.startsWith(window.API_BASE) || /^https:\/\/(api|image)\.themoviedb\.org|^https:\/\/image\.tmdb\.org/i.test(url)) return webFetch(input, init);
|
|
const hd = {};
|
|
new Headers(init.headers || (isReq ? input.headers : undefined)).forEach((v, k) => { hd[k] = v });
|
|
const sig = init.signal;
|
|
if (sig && sig.aborted) return Promise.reject(abortErr());
|
|
const call = N.fetch({
|
|
url,
|
|
method: String(init.method || (isReq ? input.method : 'GET')).toUpperCase(),
|
|
headers: hd,
|
|
body: init.body == null ? undefined : String(init.body),
|
|
timeout: init.timeout || 120000,
|
|
redirect: init.redirect || 'follow',
|
|
maxBytes: init.maxBytes || 0
|
|
}).then(wrap);
|
|
if (!sig) return call;
|
|
return Promise.race([call, new Promise((_, rej) => sig.addEventListener('abort', () => rej(abortErr()), { once: true }))]);
|
|
};
|
|
|
|
let booted = false;
|
|
const boot = () => { if (!booted) { VylaLocal.init(K); booted = true } };
|
|
const opt = v => v === undefined || v === null || v === '' || v === 'null' || v === 'undefined' ? undefined : String(v);
|
|
|
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
const OKS = [200, 206, 304];
|
|
const SEG = { recap: 'Skip Recap', intro: 'Skip Intro', preview: 'Skip Preview', credits: 'Skip Credits' };
|
|
|
|
const probeGet = (u, h, max) => fetch(u, { headers: { 'User-Agent': UA, Accept: '*/*', 'Accept-Language': 'en-US,en;q=0.9', ...h, Range: 'bytes=0-1024' }, timeout: 6000, maxBytes: max });
|
|
|
|
const playable = async (url, h, depth = 0) => {
|
|
try {
|
|
const r = await probeGet(url, h, 1048576);
|
|
if (!OKS.includes(r.status)) return false;
|
|
const ct = (r.headers.get('content-type') || '').toLowerCase();
|
|
if (!(url.includes('.m3u8') || ct.includes('mpegurl') || ct.includes('m3u8'))) return true;
|
|
if (depth > 4) return false;
|
|
const line = (await r.text()).split('\n').map(x => x.trim()).find(x => x && x[0] !== '#');
|
|
if (!line) return false;
|
|
const base = url.slice(0, url.lastIndexOf('/') + 1), origin = new URL(url).origin;
|
|
const t = line.startsWith('http') ? line : line.startsWith('/') ? origin + line : base + line;
|
|
if (t.includes('m3u8')) return playable(t, h, depth + 1);
|
|
return OKS.includes((await probeGet(t, h, 1024)).status);
|
|
} catch (_) { return false }
|
|
};
|
|
|
|
window.Vyla = {
|
|
sources() {
|
|
boot();
|
|
return VylaLocal.sources().map(c => ({ key: c.key, label: c.label }));
|
|
},
|
|
async stream(provider, tmdbId, season, episode) {
|
|
boot();
|
|
try {
|
|
const d = await VylaLocal.stream(String(provider), String(tmdbId), opt(season), opt(episode));
|
|
if (!d) return { success: false };
|
|
const h = d.headers || {};
|
|
if (Array.isArray(d.allUrls) && d.allUrls.length) {
|
|
const ok = await Promise.all(d.allUrls.map(async u => (u && u.url && await playable(u.url, h)) ? u : null));
|
|
d.allUrls = ok.filter(Boolean);
|
|
if (d.allUrls.length) { if (!d.url || !d.allUrls.some(u => u.url === d.url)) d.url = d.allUrls[0].url }
|
|
else d.url = null;
|
|
} else if (d.url && !(await playable(d.url, h))) d.url = null;
|
|
return d.url ? { success: true, provider, stream: d } : { success: false };
|
|
} catch (_) { return { success: false } }
|
|
},
|
|
async subtitles(id, season, episode) {
|
|
boot();
|
|
try { return (await VylaLocal.subtitles(String(id), opt(season), opt(episode))) || [] } catch (_) { return [] }
|
|
},
|
|
async timestamps(id, season, episode, durationSec) {
|
|
let u = `https://api.theintrodb.org/v3/media?tmdb_id=${id}`;
|
|
if (season) u += `&season=${season}`;
|
|
if (episode) u += `&episode=${episode}`;
|
|
if (durationSec) u += `&duration_ms=${Math.round(durationSec * 1000)}`;
|
|
try {
|
|
const r = await fetch(u, { timeout: 8000 });
|
|
if (!r.ok) return [];
|
|
const d = await r.json(), out = [];
|
|
Object.keys(SEG).forEach(t => (Array.isArray(d[t]) ? d[t] : []).forEach(x => out.push({ type: t, label: SEG[t], start: x.start_ms != null ? x.start_ms / 1000 : 0, end: x.end_ms != null ? x.end_ms / 1000 : null })));
|
|
return out;
|
|
} catch (_) { return [] }
|
|
}
|
|
};
|
|
})(); |