Files
TV/js/native.js

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 [] }
}
};
})();