Перенаправляет открытие acestream:// ссылки в VLC Player (vlc:// протокол с fallback на M3U)
// ==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);
})();