AceStream → VLC

Перенаправляет открытие acestream:// ссылки в VLC Player (vlc:// протокол с fallback на M3U)

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)

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)

// ==UserScript==
// @name         AceStream → VLC
// @name:en      AceStream → VLC
// @namespace    acestream-vlc
// @version      1.3.1
// @description  Перенаправляет открытие acestream:// ссылки в VLC Player (vlc:// протокол с fallback на M3U)
// @description:en Redirects links in the ‘acestream://’ format to VLC Player (using the ‘vlc://’ protocol with an M3U fallback)
// @author       Kimi + Claude + you
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// @compatible   firefox 52+ Violentmonkey
// @compatible   firefox 52+ Tampermonkey
// @compatible   chrome  55+ Violentmonkey
// @compatible   chrome  55+ Tampermonkey
// @compatible   chrome  55+ ScriptCat
// @compatible   edge 79+ Tampermonkey
// @compatible   opera 55+ Tampermonkey
// @compatible   safari 11+ Stay
// @compatible   android Via Browser
// ==/UserScript==

(function () {
    'use strict';

    // ══════════════════════════════════════════════════════════════
    //  НАСТРОЙКИ
    // ══════════════════════════════════════════════════════════════

    const DEBUG = true;   // true  → пишем всё в консоль браузера (F12)
                           // false → тишина в консоли (для повседневного использования)

    const VLC_PROTO = true;   // true  → пробуем vlc:// с fallback на M3U
                               // false → сразу скачиваем M3U

    const ENGINE_HOST    = 'http://127.0.0.1:6878';
    const STREAM_PATH    = '/ace/getstream';
    const PLAYLIST_EXT   = '.m3u';

    /** Сколько мс ждать реакции ОС на vlc:// перед fallback.
     *  Если vlc:// зарегистрирован вручную (см. .reg / .desktop) —
     *  можно смело уменьшить до 800, отклик будет быстрее. */
    const PROTO_TIMEOUT  = 1500;

    // ══════════════════════════════════════════════════════════════

    const log  = (...args) => { if (DEBUG) console.info('[AceStream→VLC]', ...args); };
    const warn = (...args) => { if (DEBUG) console.warn('[AceStream→VLC]', ...args); };

    function buildStreamUrl(hash) {
        return `${ENGINE_HOST}${STREAM_PATH}?id=${hash}`;
    }

    function extractHash(raw) {
        return raw.replace(/^acestream:\/\//i, '').trim();
    }

    // ─── Метод 1: vlc:// протокол ─────────────────────────────────
    /**
     * Пытается открыть поток через vlc:// URI.
     * Возвращает Promise<boolean> — true если, судя по blur, сработало.
     *
     * Механизм детекта:
     *   Браузер теряет фокус (window blur), когда ОС передаёт URI
     *   внешнему приложению. Если blur не пришёл за PROTO_TIMEOUT мс —
     *   считаем, что протокол не зарегистрирован.
     *
     * ВАЖНО (fix v1.2.1, актуально и для v1.3.0):
     *   Схему "http://" убираем ЗДЕСЬ, ДО того как завернуть ссылку
     *   в vlc://. В v1.2 это не делалось — получалось "vlc://http://...",
     *   и браузер видит вложенную схему внутри opaque-пути "vlc:",
     *   из-за чего ':' и '/' внутри percent-encode'ятся (http%3A%2F%2F...)
     *   ещё на стороне браузера, ДО того как строка вообще попадёт
     *   в реестровый обработчик. Тот потом либо не может нормально её
     *   раскодировать, либо раскодирует не полностью — VLC получает
     *   мусор и трактует его как относительный путь к файлу (отсюда
     *   ошибка "file:///C:/Windows/system32/http%2F%2F...").
     *   Без вложенной схемы (vlc://host:port/path?query) URL остаётся
     *   валидным сам по себе — authority (host:port), path и query
     *   разбираются штатно, ничего не экранируется, манглинга нет.
     *   Именно эта чистая форма и позволяет в v1.3.0 обойтись одним
     *   .reg файлом (powershell вызывается напрямую из реестра, без
     *   промежуточного .bat).
     */
    function tryVlcProto(streamUrl) {
        return new Promise((resolve) => {
            let resolved = false;

            function onBlur() {
                if (resolved) return;
                resolved = true;
                cleanup();
                log('vlc:// протокол сработал ✓');
                resolve(true);
            }

            function cleanup() {
                window.removeEventListener('blur', onBlur);
            }

            window.addEventListener('blur', onBlur);

            // Срезаем http(s):// ДО оборачивания в vlc:// — см. комментарий выше
            const vlcUri = `vlc://${streamUrl.replace(/^https?:\/\//i, '')}`;
            log('Пробую vlc://:', vlcUri);

            // Открываем URI в скрытом iframe — безопаснее, чем менять location
            // (не навигирует основную страницу, не даёт "Покинуть сайт?" диалог)
            const iframe = document.createElement('iframe');
            iframe.style.cssText = 'display:none;width:0;height:0;border:0;';
            document.body.appendChild(iframe);

            try {
                iframe.src = vlcUri;
            } catch (e) {
                // Некоторые браузеры бросают исключение на нестандартный протокол
                warn('Исключение при vlc://:', e.message);
            }

            setTimeout(() => {
                if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
                if (!resolved) {
                    resolved = true;
                    cleanup();
                    warn('vlc:// не ответил за', PROTO_TIMEOUT, 'мс');
                    resolve(false);
                }
            }, PROTO_TIMEOUT);
        });
    }

    // ─── Метод 2: M3U blob (fallback) ─────────────────────────────
    function openViaPlaylist(hash, streamUrl) {
        log('Fallback: создаю M3U плейлист...');

        const m3u = [
            '#EXTM3U',
            `#EXTINF:-1,AceStream ${hash.slice(0, 8)}`,
            streamUrl
        ].join('\n');

        const blob    = new Blob([m3u], { type: 'audio/x-mpegurl' });
        const blobUrl = URL.createObjectURL(blob);

        const a = Object.assign(document.createElement('a'), {
            href:     blobUrl,
            download: `ace_${hash.slice(0, 12)}${PLAYLIST_EXT}`
        });

        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);

        setTimeout(() => URL.revokeObjectURL(blobUrl), 10_000);
        log('M3U скачан, поток:', streamUrl);
    }

    // ─── Основная логика открытия ──────────────────────────────────
    async function openStream(hash) {
        const streamUrl = buildStreamUrl(hash);

        if (VLC_PROTO) {
            const ok = await tryVlcProto(streamUrl);
            if (!ok) {
                // Протокол не зарегистрирован или не сработал → M3U fallback
                openViaPlaylist(hash, streamUrl);
            }
        } else {
            // Флаг выключен → сразу M3U
            openViaPlaylist(hash, streamUrl);
        }
    }

    // ─── Перехват кликов по <a href="acestream://..."> ─────────────
    document.addEventListener('click', function (e) {
        const anchor = e.target.closest('a[href]');
        if (!anchor) return;

        const href = anchor.getAttribute('href') || '';
        if (!/^acestream:\/\//i.test(href)) return;

        e.preventDefault();
        e.stopImmediatePropagation();

        const hash = extractHash(href);
        if (!hash) {
            warn('Пустой hash в:', href);
            return;
        }

        openStream(hash);
    }, true);

    // ─── Перехват window.open('acestream://...') ───────────────────
    const _winOpen = window.open.bind(window);
    window.open = function (url, ...rest) {
        if (typeof url === 'string' && /^acestream:\/\//i.test(url)) {
            openStream(extractHash(url));
            return null;
        }
        return _winOpen(url, ...rest);
    };

    log('Скрипт загружен. VLC_PROTO =', VLC_PROTO, '| DEBUG =', DEBUG);

})();