Iwara Keepalive

Keeps iwara.tv video stream URLs fresh so idle-then-play and pause/resume work without a page reload.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

Advertisement:

// ==UserScript==
// @name         Iwara Keepalive
// @name:zh-CN Iwara Keepalive
// @name:zh-TW Iwara Keepalive
// @name:ja Iwara Keepalive
// @name:ko Iwara Keepalive
// @namespace    iwara-keepalive
// @version      0.1.0
// @description  Keeps iwara.tv video stream URLs fresh so idle-then-play and pause/resume work without a page reload.
// @description:zh-CN 持续更新 iwara.tv 视频流 URL,让闲置后播放和暂停/继续无需重新加载页面。
// @description:zh-TW 持續更新 iwara.tv 影片串流 URL,讓閒置後播放與暫停/繼續不必重新載入頁面。
// @description:ja iwara.tv の動画ストリーム URL を更新し、放置後の再生や一時停止/再開をページ再読み込みなしで動かします。
// @description:ko iwara.tv 동영상 스트림 URL을 최신 상태로 유지해 대기 후 재생과 일시정지/재개가 페이지 새로고침 없이 동작하게 합니다.
// @author       0xdev
// @license      LGPL-3.0-or-later
// @match        https://www.iwara.tv/*
// @match        https://iwara.tv/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: keepalive/iwara-keepalive.user.js
'use strict';
const NEAR_EXPIRY_MARGIN_SEC = 90;
function parseExpirySeconds(url) {
    let raw;
    try {
        raw = new URL(url, 'https://www.iwara.tv').searchParams.get('expires');
    }
    catch {
        return null;
    }
    if (raw == null || raw === '')
        return null;
    const n = Number(raw);
    if (!Number.isFinite(n) || n <= 0)
        return null;
    return n > 1e12 ? Math.floor(n / 1000) : n;
}
function classifyFreshness(url, nowSec, marginSec = NEAR_EXPIRY_MARGIN_SEC) {
    const exp = parseExpirySeconds(url);
    if (exp == null)
        return 'stale';
    if (nowSec >= exp)
        return 'stale';
    if (nowSec >= exp - marginSec)
        return 'near';
    return 'fresh';
}
function isStaleOrNear(url, nowSec, marginSec = NEAR_EXPIRY_MARGIN_SEC) {
    const f = classifyFreshness(url, nowSec, marginSec);
    return f === 'stale' || f === 'near';
}
function extractVideoId(pathname) {
    const m = /^\/video\/([^/]+)/.exec(pathname || '');
    return m ? m[1] : null;
}
function toAbsoluteUrl(url) {
    return url.startsWith('//') ? `https:${url}` : url;
}
function parseVariantName(viewUrl) {
    let filename;
    try {
        filename = new URL(viewUrl, 'https://www.iwara.tv').searchParams.get('filename');
    }
    catch {
        return null;
    }
    if (!filename)
        return null;
    const m = /_([^_.]+)\.[a-z0-9]+$/i.exec(filename);
    return m ? m[1] : null;
}
function pickVariant(variants, currentViewUrl) {
    if (!Array.isArray(variants) || variants.length === 0)
        return null;
    const byName = (name) => variants.find((v) => v && v.name === name);
    const desired = parseVariantName(currentViewUrl);
    return ((desired ? byName(desired) : undefined) ||
        byName('Source') ||
        variants.find((v) => v && v.name !== 'preview') ||
        variants[0]);
}
function shouldRefresh(currentUrl, nowSec, opts = {}) {
    if (opts.force)
        return true;
    return isStaleOrNear(currentUrl, nowSec, opts.marginSec);
}
const HARVEST_HEADER_KEYS = ['authorization', 'x-version', 'x-site'];
function pickHarvestableHeaders(url, headers) {
    let host;
    try {
        host = new URL(url, 'https://www.iwara.tv').host;
    }
    catch {
        return null;
    }
    if (!/(^|\.)iwara\.tv$/i.test(host))
        return null;
    const out = {};
    for (const k of HARVEST_HEADER_KEYS) {
        if (headers && headers[k])
            out[k] = headers[k];
    }
    return Object.keys(out).length ? out : null;
}
// Deliberately NO `credentials: 'include'`: the API only returns a readable
// CORS response in non-credentialed mode. With cookies it omits
// Access-Control-Allow-Origin and the browser blocks the fetch. iwara authorizes
// via the Bearer token we harvest into `headers`, not cookies.
async function fetchFreshVariants(videoId, headers, fetchImpl = fetch) {
    const base = { 'x-site': 'www.iwara.tv', ...(headers || {}) };
    const metaRes = await fetchImpl(`https://api.iwara.tv/video/${encodeURIComponent(videoId)}`, { headers: base });
    if (!metaRes.ok)
        throw new Error(`metadata ${metaRes.status}`);
    const meta = (await metaRes.json());
    if (!meta || !meta.fileUrl)
        throw new Error('no fileUrl in metadata');
    const manRes = await fetchImpl(meta.fileUrl, { headers: base });
    if (!manRes.ok)
        throw new Error(`manifest ${manRes.status}`);
    const variants = (await manRes.json());
    if (!Array.isArray(variants))
        throw new Error('manifest not an array');
    return variants;
}
async function selectFreshViewUrl(videoId, currentViewUrl, headers, fetchImpl = fetch) {
    const variants = await fetchFreshVariants(videoId, headers, fetchImpl);
    const variant = pickVariant(variants, currentViewUrl);
    if (!variant || !variant.src || !variant.src.view)
        throw new Error('no usable variant');
    return toAbsoluteUrl(variant.src.view);
}
function isVideoJsPlayer(value) {
    return !!value && typeof value === 'object' && typeof value.src === 'function';
}
function findVideoJsPlayer(video) {
    const win = video.ownerDocument.defaultView;
    const videojs = win?.videojs;
    const playerEl = video.closest('.video-js');
    const attachedPlayer = video.player || playerEl?.player;
    if (isVideoJsPlayer(attachedPlayer))
        return attachedPlayer;
    if (!videojs)
        return null;
    const candidates = [video];
    if (playerEl) {
        candidates.push(playerEl);
        if (playerEl.id)
            candidates.push(playerEl.id);
    }
    if (typeof videojs.getPlayer === 'function') {
        for (const candidate of candidates) {
            try {
                const player = videojs.getPlayer(candidate);
                if (isVideoJsPlayer(player))
                    return player;
            }
            catch { }
        }
    }
    if (playerEl?.id) {
        const player = videojs.players?.[playerEl.id];
        if (isVideoJsPlayer(player))
            return player;
    }
    return null;
}
function mediaTypeForUrl(viewUrl) {
    let sourceName;
    try {
        const url = new URL(viewUrl, 'https://www.iwara.tv');
        sourceName = url.searchParams.get('filename') || url.pathname;
    }
    catch {
        sourceName = viewUrl;
    }
    const lower = sourceName.toLowerCase();
    if (lower.endsWith('.mp4'))
        return 'video/mp4';
    if (lower.endsWith('.webm'))
        return 'video/webm';
    if (lower.endsWith('.m3u8'))
        return 'application/x-mpegURL';
    return undefined;
}
function readPlaybackState(video, player) {
    let at = video.currentTime;
    let wasPlaying = !video.paused;
    let rate = video.playbackRate;
    try {
        const playerTime = player?.currentTime?.();
        if (typeof playerTime === 'number' && Number.isFinite(playerTime))
            at = playerTime;
    }
    catch { }
    try {
        const playerPaused = player?.paused?.();
        if (typeof playerPaused === 'boolean')
            wasPlaying = !playerPaused;
    }
    catch { }
    try {
        const playerRate = player?.playbackRate?.();
        if (typeof playerRate === 'number' && Number.isFinite(playerRate))
            rate = playerRate;
    }
    catch { }
    return { at, wasPlaying, rate };
}
function restorePlayback(video, player, at, rate, wasPlaying) {
    try {
        if (player?.currentTime)
            player.currentTime(at);
        else
            video.currentTime = at;
    }
    catch { /* seek may be clamped; ignore */ }
    try {
        if (player?.playbackRate)
            player.playbackRate(rate);
        else
            video.playbackRate = rate;
    }
    catch { }
    if (wasPlaying) {
        try {
            const p = player?.play ? player.play() : video.play();
            if (p && typeof p.catch === 'function')
                p.catch(() => { });
        }
        catch { }
    }
}
function onceLoadedMetadata(video, player, handler) {
    if (player?.one) {
        try {
            player.one('loadedmetadata', handler);
            return;
        }
        catch { }
    }
    video.addEventListener('loadedmetadata', handler, { once: true });
}
function lockVideoJsLayout(video, player) {
    if (!player)
        return () => { };
    const playerEl = video.closest('.video-js');
    if (!playerEl)
        return () => { };
    const rect = playerEl.getBoundingClientRect();
    if (!Number.isFinite(rect.height) || rect.height <= 0)
        return () => { };
    const previousMinHeight = playerEl.style.minHeight;
    playerEl.style.minHeight = `${Math.round(rect.height)}px`;
    let released = false;
    return () => {
        if (released)
            return;
        released = true;
        playerEl.style.minHeight = previousMinHeight;
    };
}
function swapSource(video, viewUrl) {
    const freshUrl = toAbsoluteUrl(viewUrl);
    const player = findVideoJsPlayer(video);
    const { at, wasPlaying, rate } = readPlaybackState(video, player);
    const unlockLayout = lockVideoJsLayout(video, player);
    let restored = false;
    const restore = () => {
        if (restored)
            return;
        restored = true;
        restorePlayback(video, player, at, rate, wasPlaying);
        unlockLayout();
    };
    if (player?.src) {
        onceLoadedMetadata(video, player, restore);
        const source = { src: freshUrl };
        const type = mediaTypeForUrl(freshUrl);
        if (type)
            source.type = type;
        try {
            player.src(source);
            return;
        }
        catch { }
    }
    video.addEventListener('loadedmetadata', restore, { once: true });
    video.src = freshUrl;
    video.load();
}
if (typeof window !== 'undefined')
    (function main() {
        const LOG = '[iwara-keepalive]';
        const debug = (...a) => { try {
            console.debug(LOG, ...a);
        }
        catch { /* console may be gone */ } };
        let harvestedHeaders = {};
        const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
        if (nativeFetch) {
            window.fetch = function keepaliveFetch(input, init) {
                try {
                    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
                    const raw = {};
                    const collect = (h) => {
                        if (!h)
                            return;
                        if (h instanceof Headers)
                            h.forEach((v, k) => { raw[k.toLowerCase()] = v; });
                        else if (Array.isArray(h))
                            for (const [k, v] of h)
                                raw[k.toLowerCase()] = v;
                        else
                            for (const k of Object.keys(h))
                                raw[k.toLowerCase()] = h[k];
                    };
                    if (input instanceof Request)
                        collect(input.headers);
                    if (init && init.headers)
                        collect(init.headers);
                    const picked = pickHarvestableHeaders(url, raw);
                    if (picked)
                        harvestedHeaders = { ...harvestedHeaders, ...picked };
                }
                catch { /* harvesting is best-effort; never break the site's fetch */ }
                return nativeFetch(input, init);
            };
        }
        let refreshInFlight = false;
        const maybeRefresh = async (video, opts = {}) => {
            if (refreshInFlight)
                return;
            const videoId = extractVideoId(location.pathname);
            if (!videoId)
                return;
            const current = video.currentSrc || video.src || '';
            const nowSec = Date.now() / 1000;
            if (!shouldRefresh(current, nowSec, opts))
                return;
            refreshInFlight = true;
            try {
                const fresh = await selectFreshViewUrl(videoId, current, harvestedHeaders);
                if (fresh && fresh !== current) {
                    debug('refreshing stale source', opts.force ? '(forced)' : '');
                    swapSource(video, fresh);
                }
            }
            catch (e) {
                debug('refresh failed:', e instanceof Error ? e.message : e);
            }
            finally {
                refreshInFlight = false;
            }
        };
        const BOUND = new WeakSet();
        const bindVideo = (video) => {
            if (!video || BOUND.has(video))
                return;
            BOUND.add(video);
            video.addEventListener('play', () => { maybeRefresh(video); });
            video.addEventListener('seeking', () => { maybeRefresh(video); });
            video.addEventListener('error', () => {
                if (video.error && video.error.code === 4)
                    maybeRefresh(video, { force: true });
            });
            debug('bound video element');
        };
        const scanAndBind = () => { document.querySelectorAll('video').forEach(bindVideo); };
        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState !== 'visible')
                return;
            document.querySelectorAll('video').forEach((v) => maybeRefresh(v));
        });
        let scanTimer = null;
        const observer = new MutationObserver(() => {
            if (scanTimer)
                return;
            scanTimer = setTimeout(() => { scanTimer = null; scanAndBind(); }, 200);
        });
        const start = () => {
            scanAndBind();
            observer.observe(document.documentElement, { childList: true, subtree: true });
            debug('active');
        };
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', start, { once: true });
        }
        else {
            start();
        }
    })();
if (typeof window === 'undefined' && typeof module !== 'undefined' && module.exports) {
    module.exports = {
        NEAR_EXPIRY_MARGIN_SEC,
        parseExpirySeconds,
        classifyFreshness,
        isStaleOrNear,
        extractVideoId,
        toAbsoluteUrl,
        parseVariantName,
        pickVariant,
        shouldRefresh,
        HARVEST_HEADER_KEYS,
        pickHarvestableHeaders,
        fetchFreshVariants,
        selectFreshViewUrl,
        swapSource,
    };
}