AceStream → VLC

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);

})();