mirror of
https://gitlab.com/vyla-entertainment/TV.git
synced 2026-11-09 17:47:20 -05:00
794 lines
27 KiB
JavaScript
794 lines
27 KiB
JavaScript
let currentHls = null;
|
|
let currentBgHls = null;
|
|
let currentSources = [];
|
|
let resolvedStreams = [];
|
|
let selectedSourceKey = null;
|
|
let selectedStreamUrl = null;
|
|
let expandedSources = new Set();
|
|
let currentQualityLevels = [];
|
|
let selectedQualityIndex = -1;
|
|
let activeSubtitles = [];
|
|
let mediaSegments = [];
|
|
let activeSegment = null;
|
|
let plPrev = null;
|
|
let plT = null;
|
|
let plItem = null;
|
|
let pmK = null;
|
|
let plCtx = null;
|
|
let viewS = null;
|
|
let drag = 0;
|
|
let discoverySession = 0;
|
|
let userHasManuallySelectedSource = false;
|
|
|
|
let subSettings = {
|
|
size: 24,
|
|
color: '#ffffff',
|
|
bg: 'rgba(0,0,0,0.75)',
|
|
offset: 0
|
|
};
|
|
|
|
const pi = (p, f) => `<svg viewBox="0 0 24 24" fill="${f ? 'currentColor' : 'none'}" stroke="${f ? 'none' : 'currentColor'}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
|
|
const SPK = '<path d="M4 9v6h4l5 4V5L8 9z"/>';
|
|
const PI = {
|
|
x: pi('<path d="M5 5l14 14M19 5 5 19"/>'),
|
|
play: pi('<path d="M7 4.5v15L19.5 12z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>', 1),
|
|
pause: pi('<rect x="6" y="4.5" width="4" height="15" rx="1.2"/><rect x="14" y="4.5" width="4" height="15" rx="1.2"/>', 1),
|
|
cc: pi('<rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10a2 2 0 1 0 0 4M17 10a2 2 0 1 0 0 4"/>'),
|
|
vol: pi(SPK + '<path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a9 9 0 0 1 0 12"/>'),
|
|
mute: pi(SPK + '<path d="m17 9.5 4 5m0-5-4 5"/>'),
|
|
qlt: pi('<path d="M4 6h16M4 12h16M4 18h7"/>'),
|
|
set: pi('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>'),
|
|
more: pi('<path d="M5 12h.01M12 12h.01M19 12h.01" stroke-width="3.4"/>'),
|
|
fs: pi('<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>'),
|
|
fx: pi('<path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/>'),
|
|
eps: pi('<rect x="3" y="4" width="7" height="6" rx="1.5"/><rect x="14" y="4" width="7" height="6" rx="1.5"/><rect x="3" y="14" width="7" height="6" rx="1.5"/><rect x="14" y="14" width="7" height="6" rx="1.5"/>'),
|
|
chk: pi('<path d="m5 12.5 4.5 4.5L19 7"/>')
|
|
};
|
|
|
|
$('#ptop').innerHTML = `<div class="pic f" id="pback">${PI.x}</div><img class="av" id="pav" alt=""><div class="pt"><b id="pt"></b><small id="ps"></small></div><div class="pr"><div class="pic f" id="pqlt" title="Quality">${PI.qlt}</div><div class="pic f" id="pcc">${PI.cc}</div><div class="pic f" id="pvol">${PI.vol}</div><div class="pic f" id="pset">${PI.set}</div><div class="pic f" id="pmore">${PI.more}</div></div>`;
|
|
$('#pbot').innerHTML = `<span class="bt" id="pcur">0:00</span><div class="sk f" id="pseek"><u id="pbuf"></u><i id="pfill"></i><b id="phd"></b></div><span class="bt" id="pdur">0:00</span><div class="pic f" id="peps">${PI.eps}</div><div class="pic f" id="pfs">${PI.fs}</div>`;
|
|
$('#pmid').innerHTML = PI.play;
|
|
|
|
const pm = $('#pm');
|
|
const sk = $('#pseek');
|
|
const pbg = $('#pbg');
|
|
|
|
const fmt = t => {
|
|
t = Math.max(0, Math.floor(t || 0));
|
|
const h = Math.floor(t / 3600);
|
|
const m = Math.floor((t % 3600) / 60);
|
|
const c = t % 60;
|
|
return (h ? h + ':' + String(m).padStart(2, '0') : m) + ':' + String(c).padStart(2, '0');
|
|
};
|
|
|
|
const tgl = () => vd.paused ? vd.play() : vd.pause();
|
|
const pico = () => {
|
|
$('#pmid').innerHTML = vd.paused ? PI.play : PI.pause;
|
|
};
|
|
|
|
function showCtl() {
|
|
pl.classList.add('ui');
|
|
clearTimeout(plT);
|
|
plT = setTimeout(() => {
|
|
if (!vd.paused && !pmK && !pl.classList.contains('eps')) {
|
|
pl.classList.remove('ui');
|
|
}
|
|
}, 3800);
|
|
}
|
|
|
|
function closeMenu(rf) {
|
|
if (!pmK) return;
|
|
const t = {
|
|
qlt: '#pqlt',
|
|
cc: '#pcc',
|
|
vol: '#pvol',
|
|
set: '#pset',
|
|
more: '#pmore'
|
|
}[pmK];
|
|
pmK = null;
|
|
pm.classList.remove('open');
|
|
pm.innerHTML = '';
|
|
if (rf) setFocus($(t));
|
|
}
|
|
|
|
function updateSubStyles() {
|
|
vd.classList.add('sub-active');
|
|
vd.style.setProperty('--sub-size', `${subSettings.size}px`);
|
|
vd.style.setProperty('--sub-color', subSettings.color);
|
|
vd.style.setProperty('--sub-bg', subSettings.bg);
|
|
Array.from(vd.textTracks).forEach(track => {
|
|
if (track.cues) {
|
|
for (let i = 0; i < track.cues.length; i++) {
|
|
const cue = track.cues[i];
|
|
if (cue && !cue._initialOffsetApplied) {
|
|
cue.startTime += subSettings.offset;
|
|
cue.endTime += subSettings.offset;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function applySubOffset(delta) {
|
|
subSettings.offset += delta;
|
|
Array.from(vd.textTracks).forEach(track => {
|
|
if (track.cues) {
|
|
for (let i = 0; i < track.cues.length; i++) {
|
|
const cue = track.cues[i];
|
|
if (cue) {
|
|
cue.startTime += delta;
|
|
cue.endTime += delta;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildProxyUrl(rawUrl, headersObj = {}) {
|
|
return `${API_BASE}/api/proxy?url=${encodeURIComponent(rawUrl)}&headers=${encodeURIComponent(JSON.stringify(headersObj))}`;
|
|
}
|
|
|
|
async function fetchSources() {
|
|
currentSources = Vyla.sources();
|
|
}
|
|
|
|
async function startConcurrentDiscovery(it, s = null, e = null) {
|
|
const session = ++discoverySession;
|
|
pl.classList.add('buf');
|
|
|
|
resolvedStreams = [];
|
|
selectedStreamUrl = null;
|
|
let foundFirstPlayable = false;
|
|
|
|
if (!currentSources.length) {
|
|
await fetchSources();
|
|
}
|
|
|
|
if (session !== discoverySession) return;
|
|
|
|
const sourcePromises = currentSources.map(async (src) => {
|
|
try {
|
|
const data = await Vyla.stream(src.key, it.id, s, e);
|
|
if (session !== discoverySession) return;
|
|
|
|
if (data.success && data.stream) {
|
|
const streamData = data.stream;
|
|
const headers = streamData.headers || {};
|
|
let playable = [];
|
|
|
|
if (Array.isArray(streamData.allUrls) && streamData.allUrls.length > 0) {
|
|
playable = streamData.allUrls;
|
|
} else if (streamData.url) {
|
|
playable = [{ url: streamData.url, quality: streamData.quality || 'Auto', isM3U8: streamData.isM3U8 }];
|
|
}
|
|
|
|
if (playable.length > 0) {
|
|
const resolvedEntry = {
|
|
provider: src.key,
|
|
label: src.label || src.key,
|
|
headers,
|
|
primaryUrl: playable[0].url,
|
|
allUrls: playable
|
|
};
|
|
|
|
resolvedStreams.push(resolvedEntry);
|
|
|
|
if (pmK === 'set') {
|
|
renderSourcesMenu();
|
|
}
|
|
|
|
if (!foundFirstPlayable && !userHasManuallySelectedSource) {
|
|
foundFirstPlayable = true;
|
|
selectedSourceKey = src.key;
|
|
playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers);
|
|
} else if (userHasManuallySelectedSource && selectedSourceKey === src.key && !selectedStreamUrl) {
|
|
playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers);
|
|
}
|
|
}
|
|
}
|
|
} catch (_) { }
|
|
});
|
|
|
|
Promise.allSettled(sourcePromises).then(() => {
|
|
if (session !== discoverySession) return;
|
|
if (!resolvedStreams.length) {
|
|
pl.classList.remove('buf');
|
|
toast('No playable sources available.');
|
|
}
|
|
});
|
|
|
|
loadSubtitles(it.id, s, e);
|
|
loadTimestamps(it.id, s, e);
|
|
}
|
|
|
|
function playSpecificStream(streamUrl, headers = {}) {
|
|
pl.classList.add('buf');
|
|
selectedStreamUrl = streamUrl;
|
|
|
|
if (currentHls) {
|
|
currentHls.destroy();
|
|
currentHls = null;
|
|
}
|
|
if (currentBgHls) {
|
|
currentBgHls.destroy();
|
|
currentBgHls = null;
|
|
}
|
|
|
|
const finalUrl = buildProxyUrl(streamUrl, headers);
|
|
|
|
if (streamUrl.includes('.m3u8')) {
|
|
if (Hls.isSupported()) {
|
|
currentHls = new Hls({
|
|
xhrSetup: function (xhr) {
|
|
xhr.withCredentials = false;
|
|
}
|
|
});
|
|
currentHls.loadSource(finalUrl);
|
|
currentHls.attachMedia(vd);
|
|
currentHls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
|
|
currentQualityLevels = data.levels || [];
|
|
vd.play().catch(() => { });
|
|
});
|
|
currentHls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
|
|
selectedQualityIndex = data.level;
|
|
});
|
|
|
|
if (pbg) {
|
|
currentBgHls = new Hls({
|
|
xhrSetup: function (xhr) {
|
|
xhr.withCredentials = false;
|
|
}
|
|
});
|
|
currentBgHls.loadSource(finalUrl);
|
|
currentBgHls.attachMedia(pbg);
|
|
currentBgHls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
pbg.muted = true;
|
|
if (!vd.paused) pbg.play().catch(() => { });
|
|
});
|
|
}
|
|
} else if (vd.canPlayType('application/vnd.apple.mpegurl')) {
|
|
vd.src = finalUrl;
|
|
vd.play().catch(() => { });
|
|
if (pbg) {
|
|
pbg.src = finalUrl;
|
|
pbg.muted = true;
|
|
pbg.play().catch(() => { });
|
|
}
|
|
}
|
|
} else {
|
|
currentQualityLevels = [];
|
|
vd.src = finalUrl;
|
|
vd.play().catch(() => { });
|
|
if (pbg) {
|
|
pbg.src = finalUrl;
|
|
pbg.muted = true;
|
|
pbg.play().catch(() => { });
|
|
}
|
|
}
|
|
}
|
|
|
|
function setPlaybackQuality(levelIndex) {
|
|
selectedQualityIndex = levelIndex;
|
|
if (currentHls) {
|
|
currentHls.currentLevel = levelIndex;
|
|
}
|
|
}
|
|
|
|
async function loadSubtitles(id, s, e) {
|
|
Array.from(vd.querySelectorAll('track')).forEach(t => t.remove());
|
|
activeSubtitles = [];
|
|
|
|
try {
|
|
activeSubtitles = await Vyla.subtitles(id, s, e);
|
|
|
|
activeSubtitles.forEach((sub, idx) => {
|
|
const track = document.createElement('track');
|
|
track.kind = 'subtitles';
|
|
track.label = `${sub.label} (${sub.source || 'mirror'})`;
|
|
track.srclang = sub.label.slice(0, 2).toLowerCase();
|
|
track.src = buildProxyUrl(sub.file);
|
|
track.default = idx === 0;
|
|
vd.appendChild(track);
|
|
track.track.mode = 'disabled';
|
|
});
|
|
updateSubStyles();
|
|
} catch (_) { }
|
|
}
|
|
|
|
async function loadTimestamps(tmdbId, season = null, episode = null, durationSec = null) {
|
|
mediaSegments = [];
|
|
activeSegment = null;
|
|
$('#pskip').classList.remove('v');
|
|
try {
|
|
let q = `${API_BASE}/api/timestamps?tmdb_id=${tmdbId}`;
|
|
if (season) q += `&season=${season}`;
|
|
if (episode) q += `&episode=${episode}`;
|
|
if (durationSec) q += `&duration_ms=${Math.round(durationSec * 1000)}`;
|
|
|
|
const res = await fetch(q);
|
|
const data = await res.json();
|
|
mediaSegments = data.segments || [];
|
|
} catch (_) {
|
|
mediaSegments = [];
|
|
}
|
|
}
|
|
|
|
function renderSourcesMenu() {
|
|
let container = $('#pm-src-list');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
|
|
if (!resolvedStreams.length) {
|
|
container.innerHTML = '<div style="padding:10px 14px;color:rgba(255,255,255,0.4);font-size:13px;">Probing providers...</div>';
|
|
return;
|
|
}
|
|
|
|
resolvedStreams.forEach(s => {
|
|
const grp = document.createElement('div');
|
|
grp.className = 'src-group';
|
|
|
|
const hdr = document.createElement('div');
|
|
hdr.className = 'src-header f' + (s.provider === selectedSourceKey ? ' on' : '');
|
|
|
|
const hasMulti = s.allUrls && s.allUrls.length > 1;
|
|
const countLabel = hasMulti ? ` (${s.allUrls.length})` : '';
|
|
const expandIcon = hasMulti ? `<span class="src-expand-btn">${expandedSources.has(s.provider) ? '▲' : '▼'}</span>` : '';
|
|
|
|
hdr.innerHTML = `<span>${s.label}${countLabel}</span><div>${expandIcon}${s.provider === selectedSourceKey ? PI.chk : ''}</div>`;
|
|
hdr._a = () => {
|
|
userHasManuallySelectedSource = true;
|
|
if (hasMulti && s.provider === selectedSourceKey) {
|
|
if (expandedSources.has(s.provider)) expandedSources.delete(s.provider);
|
|
else expandedSources.add(s.provider);
|
|
renderSourcesMenu();
|
|
} else {
|
|
selectedSourceKey = s.provider;
|
|
playSpecificStream(s.primaryUrl, s.headers);
|
|
renderSourcesMenu();
|
|
}
|
|
};
|
|
grp.appendChild(hdr);
|
|
|
|
if (hasMulti) {
|
|
const sub = document.createElement('div');
|
|
sub.className = 'src-sublist' + (expandedSources.has(s.provider) ? ' open' : '');
|
|
|
|
s.allUrls.forEach((u, i) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'src-subitem f' + (u.url === selectedStreamUrl ? ' on' : '');
|
|
item.innerHTML = `<span>${u.quality || 'Stream ' + (i + 1)}</span>${u.url === selectedStreamUrl ? PI.chk : ''}`;
|
|
item._a = () => {
|
|
userHasManuallySelectedSource = true;
|
|
selectedSourceKey = s.provider;
|
|
playSpecificStream(u.url, s.headers);
|
|
renderSourcesMenu();
|
|
};
|
|
sub.appendChild(item);
|
|
});
|
|
grp.appendChild(sub);
|
|
}
|
|
|
|
container.appendChild(grp);
|
|
});
|
|
}
|
|
|
|
function openMenu(k) {
|
|
if (pmK === k) {
|
|
closeMenu(1);
|
|
return;
|
|
}
|
|
closeEps();
|
|
pm.innerHTML = '';
|
|
pmK = k;
|
|
pm.classList.add('open');
|
|
let first = null;
|
|
|
|
const h = t => {
|
|
const e = document.createElement('h4');
|
|
e.textContent = t;
|
|
pm.appendChild(e);
|
|
};
|
|
|
|
const r = (l, on, fn) => {
|
|
const e = mkf('pi', `<span>${l}</span>${on ? PI.chk : ''}`, () => {
|
|
fn();
|
|
closeMenu(1);
|
|
});
|
|
pm.appendChild(e);
|
|
if (on && !first) first = e;
|
|
return e;
|
|
};
|
|
|
|
if (k === 'qlt') {
|
|
h('Stream Quality');
|
|
if (currentHls && currentQualityLevels.length > 0) {
|
|
r('Auto', selectedQualityIndex === -1, () => setPlaybackQuality(-1));
|
|
currentQualityLevels.forEach((lvl, idx) => {
|
|
const label = lvl.height ? `${lvl.height}p` : `${Math.round(lvl.bitrate / 1000)} kbps`;
|
|
r(label, selectedQualityIndex === idx, () => setPlaybackQuality(idx));
|
|
});
|
|
} else {
|
|
const activeProv = resolvedStreams.find(s => s.provider === selectedSourceKey);
|
|
if (activeProv && activeProv.allUrls.length > 1) {
|
|
activeProv.allUrls.forEach((u, idx) => {
|
|
const label = u.quality || `Stream ${idx + 1}`;
|
|
r(label, u.url === selectedStreamUrl, () => {
|
|
userHasManuallySelectedSource = true;
|
|
playSpecificStream(u.url, activeProv.headers);
|
|
});
|
|
});
|
|
} else {
|
|
r('Auto', true, () => { });
|
|
}
|
|
}
|
|
} else if (k === 'cc') {
|
|
h('Subtitles');
|
|
const T = [...vd.textTracks];
|
|
r('Off', !T.some(t => t.mode === 'showing'), () => {
|
|
T.forEach(t => t.mode = 'disabled');
|
|
});
|
|
T.forEach(t => r(t.label, t.mode === 'showing', () => {
|
|
T.forEach(u => u.mode = u === t ? 'showing' : 'disabled');
|
|
updateSubStyles();
|
|
}));
|
|
|
|
h('Subtitle Size');
|
|
[18, 22, 26, 32].forEach(sz => {
|
|
r(`${sz}px`, subSettings.size === sz, () => {
|
|
subSettings.size = sz;
|
|
updateSubStyles();
|
|
});
|
|
});
|
|
|
|
h('Subtitle Color');
|
|
[['White', '#ffffff'], ['Yellow', '#ffd700'], ['Cyan', '#00ffff']].forEach(([col, code]) => {
|
|
r(col, subSettings.color === code, () => {
|
|
subSettings.color = code;
|
|
updateSubStyles();
|
|
});
|
|
});
|
|
|
|
h('Subtitle Sync');
|
|
const syncRow = document.createElement('div');
|
|
syncRow.className = 'sub-ctrl-row';
|
|
syncRow.innerHTML = `<span>Timing Offset</span><div class="sub-btn-group"><button id="sub-min">-0.5s</button><button id="sub-plus">+0.5s</button></div>`;
|
|
pm.appendChild(syncRow);
|
|
$('#sub-min').onclick = (ev) => {
|
|
ev.stopPropagation();
|
|
applySubOffset(-0.5);
|
|
};
|
|
$('#sub-plus').onclick = (ev) => {
|
|
ev.stopPropagation();
|
|
applySubOffset(0.5);
|
|
};
|
|
} else if (k === 'vol') {
|
|
h('Volume');
|
|
const w = document.createElement('div');
|
|
w.className = 'vrow';
|
|
w.innerHTML = '<div class="pic f" id="vm"></div><div class="vs f" id="vsl"><i></i></div><span id="vpc"></span>';
|
|
pm.appendChild(w);
|
|
const vm = $('#vm');
|
|
const vs = $('#vsl');
|
|
const vu = () => {
|
|
const v = vd.muted ? 0 : vd.volume;
|
|
vs.firstChild.style.width = v * 100 + '%';
|
|
$('#vpc').textContent = Math.round(v * 100);
|
|
vm.innerHTML = $('#pvol').innerHTML = v ? PI.vol : PI.mute;
|
|
};
|
|
vm._a = () => {
|
|
vd.muted = !vd.muted;
|
|
vu();
|
|
};
|
|
vs._a = () => { };
|
|
vs._v = vu;
|
|
vs.addEventListener('click', e => {
|
|
const b = vs.getBoundingClientRect();
|
|
vd.muted = false;
|
|
vd.volume = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width));
|
|
vu();
|
|
});
|
|
vu();
|
|
first = vs;
|
|
} else if (k === 'set') {
|
|
h('Available Sources');
|
|
const srcContainer = document.createElement('div');
|
|
srcContainer.id = 'pm-src-list';
|
|
pm.appendChild(srcContainer);
|
|
renderSourcesMenu();
|
|
|
|
h('Playback Speed');
|
|
[.5, 1, 1.25, 1.5, 2].forEach(v => r(v === 1 ? 'Normal' : v + '\u00d7', vd.playbackRate === v, () => vd.playbackRate = v));
|
|
|
|
h('Aspect Ratio');
|
|
r('Fit Screen', !pl.classList.contains('cover'), () => {
|
|
pl.classList.remove('cover');
|
|
if (pbg) {
|
|
pbg.currentTime = vd.currentTime;
|
|
if (!vd.paused) pbg.play().catch(() => { });
|
|
}
|
|
});
|
|
r('Fill Screen', pl.classList.contains('cover'), () => {
|
|
pl.classList.add('cover');
|
|
if (pbg) pbg.pause();
|
|
});
|
|
} else {
|
|
h('Library');
|
|
r('Watch Later', has('wl', plItem), () => toggleLib('wl', plItem));
|
|
r('Favorites', has('fav', plItem), () => toggleLib('fav', plItem));
|
|
}
|
|
|
|
showCtl();
|
|
setFocus(first || pm.querySelector('.f'));
|
|
}
|
|
|
|
async function openPlayer(it, ep, ctx) {
|
|
if (!it) return;
|
|
plPrev = cur;
|
|
plItem = it;
|
|
plCtx = ctx || null;
|
|
userHasManuallySelectedSource = false;
|
|
$('#peps').style.display = plCtx ? '' : 'none';
|
|
pl.classList.remove('eps');
|
|
$('#pnext').classList.remove('v');
|
|
$('#pt').textContent = it.title || it.name || '';
|
|
$('#ps').textContent = ep || [yr(it), it._t === 'tv' ? 'Series' : 'Movie'].filter(Boolean).join(' \u2022 ');
|
|
$('#pav').src = it.poster_path ? I + 'w185' + it.poster_path : PH;
|
|
vd.poster = it.backdrop_path ? I + 'w1280' + it.backdrop_path : '';
|
|
|
|
pl.classList.add('show');
|
|
showCtl();
|
|
pico();
|
|
setFocus($('#pmid'));
|
|
|
|
if (plCtx) {
|
|
const epData = plCtx.eps[plCtx.i];
|
|
startConcurrentDiscovery(it, plCtx.s, epData.episode_number);
|
|
} else {
|
|
startConcurrentDiscovery(it);
|
|
}
|
|
}
|
|
|
|
function closePlayer() {
|
|
closeMenu();
|
|
closeEps();
|
|
if (currentHls) {
|
|
currentHls.destroy();
|
|
currentHls = null;
|
|
}
|
|
if (currentBgHls) {
|
|
currentBgHls.destroy();
|
|
currentBgHls = null;
|
|
}
|
|
plCtx = null;
|
|
vd.pause();
|
|
vd.removeAttribute('src');
|
|
vd.load();
|
|
if (pbg) {
|
|
pbg.pause();
|
|
pbg.removeAttribute('src');
|
|
pbg.load();
|
|
}
|
|
if (document.fullscreenElement) document.exitFullscreen();
|
|
pl.classList.remove('show', 'ui');
|
|
if (plPrev && document.body.contains(plPrev)) setFocus(plPrev);
|
|
}
|
|
|
|
$('#pback')._a = closePlayer;
|
|
$('#pmid')._a = tgl;
|
|
|
|
$('#pskip')._a = () => {
|
|
if (activeSegment) {
|
|
const target = activeSegment.end != null ? activeSegment.end + 0.5 : (vd.duration || vd.currentTime + 30);
|
|
vd.currentTime = target;
|
|
$('#pskip').classList.remove('v');
|
|
activeSegment = null;
|
|
} else {
|
|
vd.currentTime += 85;
|
|
}
|
|
};
|
|
|
|
$('#pqlt')._a = () => openMenu('qlt');
|
|
$('#pcc')._a = () => openMenu('cc');
|
|
$('#pvol')._a = () => openMenu('vol');
|
|
$('#pset')._a = () => openMenu('set');
|
|
$('#pmore')._a = () => openMenu('more');
|
|
$('#pfs')._a = () => document.fullscreenElement ? document.exitFullscreen() : pl.requestFullscreen && pl.requestFullscreen();
|
|
|
|
document.addEventListener('fullscreenchange', () => {
|
|
$('#pfs').innerHTML = document.fullscreenElement ? PI.fx : PI.fs;
|
|
});
|
|
|
|
const sp = e => {
|
|
const b = sk.getBoundingClientRect();
|
|
vd.currentTime = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)) * (vd.duration || 0);
|
|
};
|
|
|
|
sk.addEventListener('pointerdown', e => {
|
|
drag = 1;
|
|
sk._m = 1;
|
|
sk.setPointerCapture(e.pointerId);
|
|
sp(e);
|
|
});
|
|
|
|
sk.addEventListener('pointermove', e => {
|
|
if (drag) sp(e);
|
|
});
|
|
|
|
sk.addEventListener('pointerup', () => drag = 0);
|
|
|
|
sk._a = () => {
|
|
if (sk._m) {
|
|
sk._m = 0;
|
|
return;
|
|
}
|
|
tgl();
|
|
};
|
|
|
|
pl.querySelectorAll('.f').forEach(e => {
|
|
if (!e._a) e._a = () => { };
|
|
});
|
|
|
|
vd.addEventListener('click', () => {
|
|
if (pmK) closeMenu();
|
|
else if (pl.classList.contains('eps')) closeEps();
|
|
else tgl();
|
|
showCtl();
|
|
});
|
|
|
|
vd.addEventListener('play', () => {
|
|
if (!pl.classList.contains('cover') && pbg) {
|
|
pbg.muted = true;
|
|
pbg.currentTime = vd.currentTime;
|
|
pbg.play().catch(() => { });
|
|
}
|
|
});
|
|
|
|
vd.addEventListener('pause', () => {
|
|
if (pbg) pbg.pause();
|
|
});
|
|
|
|
vd.addEventListener('seeking', () => {
|
|
if (pbg) pbg.currentTime = vd.currentTime;
|
|
});
|
|
|
|
vd.addEventListener('seeked', () => {
|
|
if (pbg) pbg.currentTime = vd.currentTime;
|
|
});
|
|
|
|
vd.addEventListener('ratechange', () => {
|
|
if (pbg) pbg.playbackRate = vd.playbackRate;
|
|
});
|
|
|
|
vd.addEventListener('durationchange', () => {
|
|
$('#pdur').textContent = fmt(vd.duration);
|
|
if (vd.duration && plItem) {
|
|
const c = plCtx;
|
|
loadTimestamps(plItem.id, c ? c.s : null, c ? c.eps[c.i].episode_number : null, vd.duration);
|
|
}
|
|
});
|
|
|
|
vd.addEventListener('timeupdate', () => {
|
|
const d = vd.duration || 0;
|
|
const t = vd.currentTime;
|
|
const p = d ? (t / d) * 100 : 0;
|
|
|
|
$('#pfill').style.width = p + '%';
|
|
$('#phd').style.left = p + '%';
|
|
$('#pcur').textContent = fmt(t);
|
|
|
|
if (d && vd.buffered.length) {
|
|
$('#pbuf').style.width = (vd.buffered.end(vd.buffered.length - 1) / d * 100) + '%';
|
|
}
|
|
|
|
if (!pl.classList.contains('cover') && pbg && Math.abs(pbg.currentTime - t) > 0.35) {
|
|
pbg.currentTime = t;
|
|
}
|
|
|
|
const currentSeg = mediaSegments.find(seg => {
|
|
const afterStart = t >= seg.start;
|
|
const beforeEnd = seg.end == null ? (d ? t < d - 5 : true) : t < seg.end;
|
|
return afterStart && beforeEnd;
|
|
});
|
|
|
|
if (currentSeg) {
|
|
activeSegment = currentSeg;
|
|
$('#pskip').textContent = currentSeg.label;
|
|
$('#pskip').classList.add('v');
|
|
} else {
|
|
activeSegment = null;
|
|
$('#pskip').classList.remove('v');
|
|
}
|
|
});
|
|
|
|
['play', 'pause', 'ended'].forEach(n => vd.addEventListener(n, pico));
|
|
['loadstart', 'waiting', 'seeking'].forEach(n => vd.addEventListener(n, () => pl.classList.add('buf')));
|
|
['playing', 'canplay', 'seeked', 'error'].forEach(n => vd.addEventListener(n, () => pl.classList.remove('buf')));
|
|
|
|
const hasNext = () => !!plCtx && (plCtx.i + 1 < plCtx.eps.length || plCtx.seasons.some(x => x.season_number > plCtx.s));
|
|
const getSeason = async s => plCtx.cache[s] || (plCtx.cache[s] = (await api(`/tv/${plCtx.it.id}/season/${s}`)).episodes || []);
|
|
|
|
async function playEp(s, i) {
|
|
const c = plCtx;
|
|
if (!c) return;
|
|
const eps = await getSeason(s);
|
|
const p = eps[i];
|
|
if (!p || plCtx !== c) return;
|
|
c.s = s;
|
|
c.i = i;
|
|
c.eps = eps;
|
|
$('#ps').textContent = `S${s} E${p.episode_number} \u2022 ${p.name || ''}`;
|
|
$('#pnext').classList.remove('v');
|
|
closeEps();
|
|
showCtl();
|
|
setFocus($('#pmid'));
|
|
userHasManuallySelectedSource = false;
|
|
startConcurrentDiscovery(c.it, s, p.episode_number);
|
|
}
|
|
|
|
function nextEp() {
|
|
const c = plCtx;
|
|
if (!hasNext()) return;
|
|
if (c.i + 1 < c.eps.length) {
|
|
playEp(c.s, c.i + 1);
|
|
} else {
|
|
playEp(c.seasons.find(x => x.season_number > c.s).season_number, 0);
|
|
}
|
|
}
|
|
|
|
function closeEps(rf) {
|
|
if (!pl.classList.contains('eps')) return;
|
|
pl.classList.remove('eps');
|
|
if (rf) setFocus($('#peps'));
|
|
}
|
|
|
|
function openEps() {
|
|
const c = plCtx;
|
|
if (!c) return;
|
|
closeMenu();
|
|
$('#peT').innerHTML = `Episodes<small>${esc(c.it.title || c.it.name || '')}</small>`;
|
|
const sr = $('#pseas');
|
|
sr.innerHTML = '';
|
|
c.seasons.forEach(x => {
|
|
sr.appendChild(mkf('chip', esc(x.name || 'Season ' + x.season_number), () => showSeason(x.season_number)));
|
|
});
|
|
pl.classList.add('eps');
|
|
showCtl();
|
|
showSeason(c.s, true);
|
|
}
|
|
|
|
async function showSeason(sn, focus) {
|
|
const c = plCtx;
|
|
const car = $('#pcar');
|
|
if (!c) return;
|
|
viewS = sn;
|
|
[...$('#pseas').children].forEach((e, i) => e.classList.toggle('act', c.seasons[i].season_number === sn));
|
|
car.innerHTML = SK.pep(8);
|
|
const eps = await getSeason(sn);
|
|
if (viewS !== sn || plCtx !== c) return;
|
|
car.innerHTML = '';
|
|
eps.forEach((p, i) => {
|
|
const n = p.episode_number;
|
|
const cur = c.s === sn && c.i === i;
|
|
const img = p.still_path ? I + 'w300' + p.still_path : (c.it.backdrop_path ? I + 'w300' + c.it.backdrop_path : PH);
|
|
car.appendChild(mkf('pe' + (cur ? ' cur' : ''), `<div class="et"><img loading="lazy" src="${img}" alt=""><i>${n}</i>${cur ? '<span class="eq"><u></u><u></u><u></u></span><span class="pg"><u></u></span>' : ''}</div><span class="pn">${esc(p.name || 'Episode ' + n)}</span><small>${p.runtime ? p.runtime + 'm' : 'Episode ' + n}</small>`, () => playEp(sn, i)));
|
|
});
|
|
const t = car.querySelector('.cur') || car.firstChild;
|
|
if (focus && t) setFocus(t);
|
|
}
|
|
|
|
$('#pep').innerHTML = `<div class="peh"><h3 id="peT"></h3><div class="row" id="pseas"></div><div class="pic f" id="pepx">${PI.x}</div></div><div class="row pcar" id="pcar"></div>`;
|
|
$('#pnext').innerHTML = 'Next Episode' + pi('<path d="M6 5.5v13l9-6.5z"/><path d="M18 5.5v13"/>');
|
|
$('#pnext')._a = nextEp;
|
|
$('#pepx')._a = () => closeEps(1);
|
|
$('#peps')._a = () => pl.classList.contains('eps') ? closeEps(1) : openEps();
|
|
|
|
vd.addEventListener('timeupdate', () => {
|
|
const d = vd.duration || 0;
|
|
const r = d - vd.currentTime;
|
|
$('#pnext').classList.toggle('v', !!d && hasNext() && r <= 300 && r > 0);
|
|
$('#pep').style.setProperty('--p', d ? (vd.currentTime / d) * 100 + '%' : '0%');
|
|
});
|
|
|
|
vd.addEventListener('ended', () => {
|
|
if (hasNext()) nextEp();
|
|
}); |