ReYohoho – No Ads + Enhancements [Ath]

Removes video ads from online streaming services in ReYohoho (Rezka, Alloha, Collaps, VideoCDN etc.). Also applies minor enhancements, if possible (extra play speeds etc.).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           ReYohoho – No Ads + Enhancements [Ath]
// @name:ru        ReYohoho – Без Рекламы + Улучшения [Ath]
// @name:uk        ReYohoho – Без Реклами + Покращення [Ath]
// @name:be        ReYohoho – Без Рэкламы + Паляпшэнні [Ath]
// @name:bg        ReYohoho – Без Реклами + Подобрения [Ath]
// @name:tt        ReYohoho – Рекламасыз + Яхшыртулар [Ath]
// @name:sl        ReYohoho – Brez Oglasov + Izboljšave [Ath]
// @name:sr        ReYohoho – Bez Reklama + Poboljšanja [Ath]
// @name:ka        ReYohoho – რეკლამის გარეშე + გაუმჯობესებები [Ath]
// @description    Removes video ads from online streaming services in ReYohoho (Rezka, Alloha, Collaps, VideoCDN etc.). Also applies minor enhancements, if possible (extra play speeds etc.).
// @description:ru Убирает рекламные ролики онлайн-кинотеатров в ReYohoho (Rezka, Alloha, Collaps, VideoCDN и т.д.). Также применяет небольшие улучшения, если возможно (дополнительные скорости проигрывания и т.п.).
// @description:uk Видаляє рекламні ролики з онлайн-сервісів для перегляду відео у ReYohoho (Rezka, Alloha, Collaps, VideoCDN тощо). Також застосовує додаткові покращення, якщо це можливо (додаткові швидкості відтворення тощо).
// @description:be Выдаляе рэкламныя ролікі з анлайн-стрымінгавых паслуг у ReYohoho (Rezka, Alloha, Collaps, VideoCDN і г.д.). Таксама ўжывае дробныя паляпшэнні, калі гэта магчыма (дадатковыя хуткасці прайгравання і г.д.).
// @description:bg Премахва видео рекламите от онлайн стрийминг услугите в ReYohoho (Rezka, Alloha, Collaps, VideoCDN и т.н.). Така също прилага малки подобрения, ако е възможно (допълнителни скорости на възпроизвеждане и т.н.).
// @description:tt Онлайн-трансляция хезмәтләрендәге ReYohoho (Rezka, Alloha, Collaps, VideoCDN һ.б.) видео рекламаларны бетерә. Шулай ук мөмкин булганда кечкенә яхшыртулар кертә (өстәмә уйнату тизлекләре һ.б.).
// @description:sl Odstrani video oglase iz spletnih pretočnih storitev v ReYohoho (Rezka, Alloha, Collaps, VideoCDN itd.). Prav tako omogoča manjše izboljšave, če je to mogoče (dodatne hitrosti predvajanja itd.).
// @description:sr Uklanja video reklame sa online striming servisa u ReYohoho (Rezka, Alloha, Collaps, VideoCDN itd.). Takođe primenjuje manja poboljšanja, ako je to moguće (dodatne brzine reprodukcije itd.).
// @description:ka ამოიღებს ვიდეო რეკლამებს ონლაინ სტრიმინგის სერვისებიდან ReYohoho-ში (Rezka, Alloha, Collaps, VideoCDN და ა.შ.). ასევე იღებს პატარა გაუმჯობესებებს, თუ ეს შესაძლებელია (დამატებითი დაკვრის სიჩქარეები და ა.შ.).
// @namespace      athari
// @author         Athari (https://github.com/Athari)
// @copyright      © Prokhorov ‘Athari’ Alexander, 2024–2025
// @license        MIT
// @homepageURL    https://github.com/Athari/AthariUserJS
// @supportURL     https://github.com/Athari/AthariUserJS/issues
// @version        1.0.1
// @icon           https://reyohoho.github.io/reyohoho/icons/favicon-32x32.png
// @match          https://*.allarknow.online/*
// @match          https://*.obrut.show/*
// @match          https://*.embess.ws/*
// @match          https://*.fotpro135alto.com/*
// @match          https://*.videoframe1.com/*
// @match          https://*.videoframe*.com/*
// @match          https://*.lumex.space/*
// @match          https://*.tv-2-kinoserial.net/*
// @match          https://*-kinoserial.net/*
// @match          https://*.rezka.*/*
// @match          https://*.hdrezka.*/*
// @match          https://*.kinopub.*/*
// @match          https://*.rezkify.*/*
// @match          https://*.rezkery.*/*
// @grant          unsafeWindow
// @run-at         document-start
// @sandbox        raw
// @require        https://cdn.jsdelivr.net/npm/@athari/[email protected]/monkeyutils.u.min.js
// @resource       script-urlpattern https://cdn.jsdelivr.net/npm/urlpattern-polyfill/dist/urlpattern.js
// @tag            athari
// ==/UserScript==

(async () => {
  'use strict'

  const { isFunction, isObject, isString, assignDeep,
    waitForCallback, waitForEvent, waitFor, withTimeout,
    matchLocation,
    throwError, attempt,
    overrideProperty, overrideFunction,
    ress, scripts, els, opts, } =
    //require("../@athari-monkeyutils/monkeyutils.u"); // TODO
    athari.monkeyutils;

  const win = unsafeWindow;
  const res = ress(), script = scripts(res);
  const el = els(document);

  Object.assign(globalThis, globalThis.URLPattern ? null : await script.urlpattern);

  const anySubdomain = "(.*\\.)?";
  const globMap = { ".": "\\.", "*": "[^\\.]+" };
  const globDomain = (s) => s.replace(/\.|\*/g, ([m]) => globMap[m] ?? m);
  const oneOfDomains = (...ds) => `(${ds.map(globDomain).join("|")})`;
  const host = {
    alloha: `${anySubdomain}${oneOfDomains("allarknow.online")}`,
    collaps: `${anySubdomain}${oneOfDomains("embess.ws")}`,
    hdvb: `${anySubdomain}${oneOfDomains("fotpro135alto.com")}`,
    turbo: `${anySubdomain}${oneOfDomains("obrut.show")}`,
    vibix: `${anySubdomain}${oneOfDomains("videoframe*.com")}`,
    videocdn: `${anySubdomain}${oneOfDomains("lumex.space")}`,
    videoseed: `${anySubdomain}${oneOfDomains("*-kinoserial.net")}`,
    hdrezka: `${anySubdomain}${oneOfDomains("rezka.*", "hdrezka.*", "kinopub.*", "rezkify.*", "rezkery.*")}`,
  };

  const loggingProxy = (o, level = 0, root = null, path = []) => new Proxy(o, new class {
    construct() {
      console.log("proxy", { o, level, root, path });
    }
    #proxies = {}
    #logProp(act, t, prop, value, args = []) {
      console.log(act, "{", root, "}", `${path.join(".")}.{`, t, `}.${prop} = `, value, ` (${typeof value})`, args);
    }
    get(t, prop) {
      let proxy = this.#proxies[prop];
      if (proxy != null) {
        this.#logProp("get", t, prop, proxy.value);
        return proxy.proxy;
      }
      const value = Reflect.get(t, prop);
      this.#logProp("get", t, prop, value);
      if (level > 1 && (isObject(value) || isFunction(value))) {
        proxy = { value, proxy: loggingProxy(value, level - 1, root ?? value, path.concat(prop)) };
        this.#proxies[prop] = proxy;
        return proxy.proxy;
      } else {
        return value;
      }
    }
    set(t, prop, value) {
      this.#logProp("set", t, prop, value);
      return Reflect.set(t, prop, value);
    }
    apply(t, self, args) {
      const value = Reflect.apply(t, self, args);
      this.#logProp("fun", t, "()", value, args);
      return value;
    }
    construct(t, args) {
      const value = Reflect.construct(t, args);
      this.#logProp("new", t, "new()", value, args);
      return value;
    }
  });

  const playSpeeds = [ 0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 3.5, 4 ];

  const configPlayerJS = {
    settings: {
      customspeeds: 1, speeds: playSpeeds.join(","),
    },
    volume: 1,
    postmessage: 1, log: 1, eventstracker: 1, // logging & messaging
    vast: 0, vast_timeout: 0, vast_volume: 0, // ad config
    preroll: "", prerolls: 0, midroll: [], midrolls: 0, // ad urls
    yamtr: 0, // counters
  };

  const fixPlayerJS = (player) => {
    if (player == null)
      return console.error("playerjs not found");
    player.api('update:vast', false);
    player.api('log', true);
    player.api('volume', 1);
    const options = overrideFunction(win.console, 'log', null, (_, v) => v, () => player.api('options'));
    if (options == null)
      return console.error("playerjs options not found");
    const elPlayer = document.querySelector(`#${options.id}`);
    assignDeep(win, { player, options });
    console.info({ player, options, el: elPlayer });
    assignDeep(options, configPlayerJS, {
      events: options.events || /*'onPlayerJSEvent'*/'PlayerjsEvents',
    });
    if (elPlayer)
      elPlayer.oncontextmenu = null;
    if (isString(options.events)) {
      const originalEvents = options.events; // gets reset for some reason
      overrideFunction(win, options.events, (onPlayerJSEvent, event, playerId, data) => {
        options.events = originalEvents;
        const ignore = event.startsWith('vast_');
        if (![ 'time' ].includes(event))
          console.info(ignore ? "event nay" : "event yay", { e: event, data, id: playerId });
        if (ignore)
          return;
        (onPlayerJSEvent ?? win.PlayerjsEvents)?.(event, playerId, data);
      })
    }
  };

  const fixPlyrConfig = (player) =>
    assignDeep(player, {
      controls: [
        'play-large',
        'play', 'rewind', 'fast-forward', 'progress', 'current-time', 'duration',
        'mute', 'volume',
        'captions', 'settings', 'pip', 'airplay', 'fullscreen',
      ],
      ads: { enabled: false, midroll: [], preroll: "" },
      speed: { options: playSpeeds },
      settings: [ 'quality', 'audio', 'captions', 'speed', 'loop', 'scale', 'bugReport' ],
      volume: 1,
      disableContextMenu: false,
      debug: false,
    });

  console.info("reyohoho no ads", location.href);

  // Alloha: Plyr
  // Configs parsed from JSON strings
  if (matchLocation(host.alloha)) {
    overrideFunction(win.JSON, 'parse', (parse, text) => {
      const json = parse(text);
      if (json.ads && json.controls) {
        console.info("plyr config original", structuredClone(json));
        fixPlyrConfig(json);
        console.info("plyr config modified", structuredClone(json));
      } else if (json.active && json.all) {
        console.info("fileList original", json);
      } else {
        console.debug("parsed json", json);
      }
      return json;
    });
  }
  // Collaps: VenomPlayer
  // Configs passed to makePlayer wrapper function. Override function after var assignments.
  else if (matchLocation(host.collaps)) {
    const adTimeouts = { loading: 0, starting: 0, toNextImp: 0, global: 0 };
    overrideProperty(win, 'adTimeouts', { log: true, set: v => assignDeep(v, adTimeouts) });
    const adCfg = { maxImpressions: 0, urls: [], exitFullscreenVideo: false, vast: { timeouts: adTimeouts } };
    overrideProperty(win, 'adsConfig', { log: true, set: v => assignDeep(v, { volume: 0, pre: adCfg, middle: adCfg, post: adCfg }) });
    const modifiedOpts = { speed: playSpeeds, theme: 'modern', /* venom/classic/metro/modern */ };
    overrideProperty(win, 'middleCount', _ => {
      overrideFunction(win, 'makePlayer', (makePlayer, opts) =>
        makePlayer(new Proxy(assignDeep(opts, modifiedOpts), new class {
          set(t, prop, v) {
            return Object.hasOwn(modifiedOpts, prop) ? true : Reflect.set(t, prop, v);
          }
        })));
      return 0;
    });
  }
  // HDVB: PlayerJS
  // Configs provided as `let` variables. Private "rek" ads config. Break script, run manually.
  else if (matchLocation(host.hdvb)) {
    let fail = true;
    const tag = { conf: { banner_show: false }, key: "", script: "" };
    const banner = { show: false, key: "", script: "" };
    const roll = { time: "", url: "" };
    overrideProperty(win, 'playerConfigs', {
      log: "player config",
      set: v => fail ? throwError("nope") : assignDeep(v, configPlayerJS, {
        events: v.events || 'onPlayerJSEvent',
        rek: {
          endtag: tag, starttag: tag, start2tag: tag, start3tag: tag,
          pausebanner: banner,
          push_roll: roll,
          midroll: [], preroll: [], pushbanner: [],
        },
      }),
    });
    const script = await waitFor(() => el.all.tag.script.filter(s => s.innerText.includes("let playerConfigs"))[0], 10000);
    if (script == null)
      return console.error("player script not found");
    fail = false;
    win.eval(script.innerText.replace("let playerConfigs", "playerConfigs"));
    fixPlayerJS(win.player);
  }
  // Turbo: PlayerJS
  // Config provided as encrypted string passed to global Player function. Wait for pljssglobal[0] assignment.
  else if (matchLocation(host.turbo)) {
    //overrideProperty(win, 'pljssglobal', v => loggingProxy(v, 3));
    let [ player0SetWait, player0Set ] = waitForCallback();
    overrideProperty(win, 'pljssglobal', v => new Proxy(v, new class {
      set(t, prop, value) {
        if (prop == "0")
          player0Set(value);
        return Reflect.set(t, prop, value);
      }
    }));
    const player = win.player = win.pljssglobal?.[0] ?? await withTimeout(player0SetWait, 10000);
    fixPlayerJS(player);
  }
  // TODO Vibix: PlayerJS
  // Configs provided as constants and passed to Playerjs constructor. Break script, run manually.
  else if (matchLocation(host.vibix)) {
    // TODO Find a way to add download  button
    overrideProperty(win, 'DownloadVideo', { get: () => (file) => win.DownloadVideo_(file) });
    await waitForEvent(document, 'DOMContentLoaded');
    const script = await waitFor(() => el.all.tag.script.filter(s => s.innerText.includes("DownloadVideo"))[0], 10000);
    if (script == null)
      return console.error("player script not found");
    overrideFunction(win, 'Playerjs', (Playerjs, opts) => new Playerjs(assignDeep(opts, configPlayerJS)));
    win.eval(script.innerText.replace("DownloadVideo", "DownloadVideo_"));
    fixPlayerJS(win.player);
  }
  // VideoCDN/Lumex: VideoJS
  // Configs provided as HTML elements, player created in external script. Wait for variables.
  else if (matchLocation(host.videocdn)) {
    await waitForEvent(document, 'DOMContentLoaded');
    const videojs = await waitFor(() => win.videojs, 10000);
    videojs.deregisterPlugin('vast');
    const player = win.player = await waitFor(() => videojs.getAllPlayers()[0], 10000);
    player.activePlugins_.vast = false;
    player.playbackRates(playSpeeds); // TODO figure out why setting playbackRates doesn't work
    const options = win.options = player.options({ playbackRates: playSpeeds });
  }
  // VideoSeed: PlayerJS
  // Configs passed to Playerjs constructor.
  else if (matchLocation(host.videoseed)) {
    overrideFunction(win, 'Playerjs', (Playerjs, opts) => new Playerjs(assignDeep(opts, configPlayerJS)));
    await waitFor(() => win.player, 10000);
    fixPlayerJS(win.player);
  }
  // Rezka: PlayerJS
  // Configs provided as `var` variables. Override assignments.
  else if (matchLocation(host.hdrezka)) {
    overrideProperty(win, 'CDNPlayerInfo', v => assignDeep(v, { preroll: "", midroll: "[]" }));
  }
  // Unknown domain
  // TODO: Try guessing provider and/or read ReYohoho's config.
  else {
    console.warn("Unexpected domain");
  }
})();