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.).

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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");
  }
})();