Greasy Fork is available in English.

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

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