AceStream → VLC

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();