您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Autoplay for Aniworld.to and S.to
当前为
// ==UserScript== // @name Aniworld.to & S.to Autoplay // @name:de Autoplay AniWorld & S.to // @description Autoplay for Aniworld.to and S.to // @description:de Autoplay für Aniworld.to und S.to // @version 2.2.0 // @match https://aniworld.to/* // @match https://s.to/* // @match https://186.2.175.5/ // @match *://*/* // @author AniPlayer // @namespace https://gf.qytechs.cn/users/1400386 // @license GPL-3.0-or-later; https://spdx.org/licenses/GPL-3.0-or-later.html // @icon https://i.imgur.com/CEZGcX6.png // @require https://cdnjs.cloudflare.com/ajax/libs/keyboardjs/2.7.0/keyboard.min.js#sha512-UrxaOZAJw5p38NProL/UrffryqdMdXFcEdyLt6eU89pH0N7KnmAe8G3ghNbH1qW5cDYdnaoEw1TcbHn8wuqAvw== // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/tweakpane.min.js#sha512-ugca4SpzfDh4VV8oj0yscIUlKxZhJd9LD5HOX4o7jOMlI/1iGYr7S4Q4Fnvx/GFXCwAivLrdHOo/7t4iYV4ehw== // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_deleteValue // @grant GM_getValue // @grant GM_listValues // @grant GM_removeValueChangeListener // @grant GM_setValue // @run-at document-body // ==/UserScript== /** * Hi! Don't change (or even resave) anything here because * by doing this in Tampermonkey you will turn off updates * of the script (idk about other script managers). * This could be restored in settings but it might be hard to find, * so it's better to reinstall the script if you're not sure */ /* jshint esversion: 11 */ /* global Tweakpane, keyboardJS */ (async function() { 'use strict'; // Domains list the script should work for const TOP_SCOPE_DOMAINS = [ 'aniworld.to', 's.to', '186.2.175.5', ]; const VIDEO_PROVIDERS = [ 'VOE', 'Vidoza', ]; const CORE_SETTINGS_LIST = { corsProxy: 'corsProxy', commlinkPollingIntervalMs: 'commlinkPollingIntervalMs', debug: 'debug', largeSkipCooldownMs: 'largeSkipCooldownMs', playOnLargeSkip: 'playOnLargeSkip', vidozaPersistentVolumeLvl: 'vidozaPersistentVolumeLvl', }; // Note that defaults are applied only on a very first run of the script const CORE_SETTINGS_DEFAULTS = { [CORE_SETTINGS_LIST.corsProxy]: 'https://aniworld-to-cors-proxy.fly.dev/', [CORE_SETTINGS_LIST.commlinkPollingIntervalMs]: 40, [CORE_SETTINGS_LIST.debug]: false, [CORE_SETTINGS_LIST.largeSkipCooldownMs]: 300, [CORE_SETTINGS_LIST.playOnLargeSkip]: true, [CORE_SETTINGS_LIST.vidozaPersistentVolumeLvl]: 0.5, }; const HOTKEYS_SETTINGS_LIST = { fastBackward: 'fastBackward', fastForward: 'fastForward', fullscreen: 'fullscreen', largeSkip: 'largeSkip', }; // Note that defaults are applied only on a very first run of the script const HOTKEYS_SETTINGS_DEFAULTS = { [HOTKEYS_SETTINGS_LIST.fastBackward]: 'left', [HOTKEYS_SETTINGS_LIST.fastForward]: 'right', [HOTKEYS_SETTINGS_LIST.fullscreen]: 'f', [HOTKEYS_SETTINGS_LIST.largeSkip]: 'v', }; const USER_SETTINGS_LIST = { fastForwardSizeS: 'fastForwardSizeS', isAutoplayEnabled: 'isAutoplayEnabled', largeSkipSizeS: 'largeSkipSizeS', markWatchedAfterS: 'markWatchedAfterS', outroSkipThresholdS: 'outroSkipThresholdS', outroSkipThresholdSpreadS: 'outroSkipThresholdSpreadS', providersPriority: 'providersPriority', }; // Note that defaults are applied only on a very first run of the script const USER_SETTINGS_DEFAULTS = { [USER_SETTINGS_LIST.fastForwardSizeS]: 10, [USER_SETTINGS_LIST.isAutoplayEnabled]: false, [USER_SETTINGS_LIST.largeSkipSizeS]: 85, [USER_SETTINGS_LIST.markWatchedAfterS]: 2 * 60, [USER_SETTINGS_LIST.outroSkipThresholdS]: 90, [USER_SETTINGS_LIST.outroSkipThresholdSpreadS]: 0, [USER_SETTINGS_LIST.providersPriority]: ( Array.from({ length: VIDEO_PROVIDERS.length }, (_, i) => i) ), }; // Can not handle nested objects class DataStore { constructor(uuid, defaultStorage = {}) { if (typeof uuid !== 'string' && typeof uuid !== 'number') { throw new Error('Expected uuid when creating DataStore'); } this.__uuid = uuid; this.__storage = defaultStorage; try { this.__storage = JSON.parse(GM_getValue(uuid)); } catch { GM_setValue(uuid, JSON.stringify(defaultStorage)); } return new Proxy(this, { get: (obj, prop) => { if (prop === 'destroy') return () => obj.__destroy(); if (prop === 'update') return updates => obj.__update(updates); return obj.__storage[prop]; }, set: (obj, prop, value) => { obj.__storage[prop] = value; GM_setValue(obj.__uuid, JSON.stringify(obj.__storage)); return true; } }); } __update(updates) { if (updates) { Object.assign(this.__storage, updates); GM_setValue(this.__uuid, JSON.stringify(this.__storage)); } else { try { this.__storage = JSON.parse(GM_getValue(this.__uuid)) || {}; } catch { this.__storage = {}; } } } __destroy() { GM_deleteValue(this.__uuid); this.__storage = {}; } } const coreSettings = new DataStore('coreSettings', CORE_SETTINGS_DEFAULTS); const hotkeysSettings = new DataStore('hotkeysSettings', HOTKEYS_SETTINGS_DEFAULTS); const userSettings = new DataStore('userSettings', USER_SETTINGS_DEFAULTS); [ [coreSettings, CORE_SETTINGS_DEFAULTS], [hotkeysSettings, HOTKEYS_SETTINGS_DEFAULTS], [userSettings, USER_SETTINGS_DEFAULTS] ].forEach(([settings, defaults]) => { Object.entries(defaults).forEach(([key, val]) => (settings[key] ??= val)); }); if ( userSettings[USER_SETTINGS_LIST.providersPriority].length !== VIDEO_PROVIDERS.length ) { userSettings[USER_SETTINGS_LIST.providersPriority] = [ ...USER_SETTINGS_DEFAULTS[USER_SETTINGS_LIST.providersPriority] ]; } // -------------------------------------- /utils --------------------------------------------- function makeId(length = 16) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let text = ''; for (let i = 0; i < length; i++) { text += chars.charAt(Math.floor(Math.random() * chars.length)); } return text; } async function sleep(ms = 0) { return new Promise(r => setTimeout(r, ms)); } function waitForNode(query, { callbackOnTimeout = false, existing = false, onceOnly = false, rootNode = document.documentElement, timeout, observerOptions = { childList: true, subtree: true, }, } = {}, callback) { if (!query) throw new Error('Query is needed'); if (!callback) throw new Error('Callback is needed'); observerOptions = Object.assign({}, observerOptions); const handledNodes = new WeakSet(); const existingNodes = rootNode.querySelectorAll(query); let timeoutId = null; if (existingNodes.length) { // Mark all as handled for a proper work when `existing` is false // to ignore them later on for (const node of existingNodes) { handledNodes.add(node); } if (existing) { if (onceOnly) { try { callback(existingNodes[0]); } catch (e) { console.error(e); } return; } else { for (const node of existingNodes) { try { callback(node); } catch (e) { console.error(e); } } } } } const observer = new MutationObserver((mutations, observer) => { for (const node of rootNode.querySelectorAll(query)) { if (handledNodes.has(node)) continue; handledNodes.add(node); try { callback(node); } catch (e) { console.error(e); } if (onceOnly) { observer.disconnect(); if (timeoutId) clearTimeout(timeoutId); return; } } }); observer.observe(rootNode, observerOptions); if (timeout !== undefined) { timeoutId = setTimeout(() => { observer.disconnect(); if (callbackOnTimeout) { try { callback(null); } catch (e) { console.error(e); } } }, timeout); } } // -------------------------------------- utils\ --------------------------------------------- /* CommLink.js - Version: 1.0.1 - Author: Haka - Description: A userscript library for cross-window communication via the userscript storage - GitHub: https://github.com/AugmentedWeb/CommLink */ class CommLinkHandler { constructor(commlinkID, configObj) { this.commlinkID = commlinkID; this.singlePacketResponseWaitTime = configObj?.singlePacketResponseWaitTime || 1500; this.maxSendAttempts = configObj?.maxSendAttempts || 3; this.statusCheckInterval = configObj?.statusCheckInterval || 1; this.silentMode = configObj?.silentMode || false; this.commlinkValueIndicator = 'commlink-packet-'; this.commands = {}; this.listeners = []; const missingGrants = [ 'GM_getValue', 'GM_setValue', 'GM_deleteValue', 'GM_listValues', ].filter(grant => !GM_info.script.grant.includes(grant)); if (missingGrants.length > 0 && !this.silentMode) { alert( `[CommLink] The following userscript grants are missing: ${missingGrants.join(', ')}. CommLink will not work.` ); } this.getStoredPackets() .filter(packet => Date.now() - packet.date > 2e4) .forEach(packet => this.removePacketByID(packet.id)); } setIntervalAsync(callback, interval = this.statusCheckInterval) { let running = true; async function loop() { while (running) { try { await callback(); await new Promise((resolve) => setTimeout(resolve, interval)); } catch { continue; } } }; loop(); return { stop: () => { running = false; return false; } }; } getUniqueID() { return ([1e7] + -1e3 + 4e3 + -8e3 + -1e11) .replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); } getCommKey(packetID) { return this.commlinkValueIndicator + packetID; } getStoredPackets() { return GM_listValues() .filter(key => key.includes(this.commlinkValueIndicator)) .map(key => GM_getValue(key)); } addPacket(packet) { GM_setValue(this.getCommKey(packet.id), packet); } removePacketByID(packetID) { GM_deleteValue(this.getCommKey(packetID)); } findPacketByID(packetID) { return GM_getValue(this.getCommKey(packetID)); } editPacket(newPacket) { GM_setValue(this.getCommKey(newPacket.id), newPacket); } send(platform, cmd, d) { return new Promise(async resolve => { const packetWaitTimeMs = this.singlePacketResponseWaitTime; const maxAttempts = this.maxSendAttempts; let attempts = 0; for (;;) { attempts++; const packetID = this.getUniqueID(); const attemptStartDate = Date.now(); const packet = { command: cmd, data: d, date: attemptStartDate, id: packetID, sender: platform, }; if (!this.silentMode) { console.log(`[CommLink Sender] Sending packet! (#${attempts} attempt):`, packet); } this.addPacket(packet); for (;;) { const poolPacket = this.findPacketByID(packetID); const packetResult = poolPacket?.result; if (poolPacket && packetResult) { if (!this.silentMode) { console.log(`[CommLink Sender] Got result for a packet (${packetID}):`, packetResult); } resolve(poolPacket.result); attempts = maxAttempts; // stop main loop break; } if (!poolPacket || Date.now() - attemptStartDate > packetWaitTimeMs) { break; } await new Promise(res => setTimeout(res, this.statusCheckInterval)); } this.removePacketByID(packetID); if (attempts === maxAttempts) break; } return resolve(null); }); } registerSendCommand(name, obj) { this.commands[name] = async (data) => { return await this.send(obj?.commlinkID || this.commlinkID, name, obj?.data || data); }; } registerListener(sender, commandHandler) { const listener = { sender, commandHandler, intervalObj: this.setIntervalAsync(this.receivePackets.bind(this), this.statusCheckInterval), }; this.listeners.push(listener); } receivePackets() { this.getStoredPackets().forEach(packet => { this.listeners.forEach(listener => { if (packet.sender === listener.sender && !packet.hasOwnProperty('result')) { const result = listener.commandHandler(packet); packet.result = result; this.editPacket(packet); if (!this.silentMode) { if (packet.result === null) { console.log('[CommLink Receiver] Possibly failed to handle packet:', packet); } else { console.log('[CommLink Receiver] Successfully handled a packet:', packet); } } } }); }); } kill() { this.listeners.forEach(listener => listener.intervalObj.stop()); } } class IframeMessenger { constructor() { this.commLink = null; this.topScopeId = null; } static messages = { AUTOPLAY_NEXT: 'AUTOPLAY_NEXT', GET_FULLSCREEN_STATE: 'GET_FULLSCREEN_STATE', MARK_CURRENT_VIDEO_SEEN: 'MARK_CURRENT_VIDEO_SEEN', OPEN_HOTKEYS_GUIDE: 'OPEN_HOTKEYS_GUIDE', TOGGLE_FULLSCREEN: 'TOGGLE_FULLSCREEN', UPDATE_SETTINGS: 'UPDATE_SETTINGS', }; async initCrossFrameConnection() { const iframeId = makeId(); const topScopeIdPromise = new Promise((resolve) => { // Top scope using GM_setValue will write its own id using iframeId as a key const valueChangeListenerId = GM_addValueChangeListener(iframeId, ( _key, _oldValue, newValue, ) => { GM_removeValueChangeListener(valueChangeListenerId); GM_deleteValue(iframeId); resolve(newValue); }); }); // This should be almost immediately picked up by a top scope GM_setValue('unboundIframeData', { id: iframeId, width: window.innerWidth, height: window.innerHeight, }); const topScopeId = await topScopeIdPromise; if (!iframeId || !topScopeId) throw new Error('Something went wrong'); this.topScopeId = topScopeId; this.commLink = new CommLinkHandler(iframeId, { silentMode: !coreSettings[CORE_SETTINGS_LIST.debug], statusCheckInterval: coreSettings[CORE_SETTINGS_LIST.commlinkPollingIntervalMs], }); this.commLink.registerSendCommand(IframeMessenger.messages.AUTOPLAY_NEXT); this.commLink.registerSendCommand(IframeMessenger.messages.GET_FULLSCREEN_STATE); this.commLink.registerSendCommand(IframeMessenger.messages.MARK_CURRENT_VIDEO_SEEN); this.commLink.registerSendCommand(IframeMessenger.messages.OPEN_HOTKEYS_GUIDE); this.commLink.registerSendCommand(IframeMessenger.messages.TOGGLE_FULLSCREEN); this.commLink.registerSendCommand(IframeMessenger.messages.UPDATE_SETTINGS); } registerConnectionListener(callback) { return this.commLink.registerListener(this.topScopeId, callback); } async sendMessage(message, msgData) { return await this.commLink.commands[message](msgData); } } class IframeInterface { constructor(messenger) { this.commLink = null; this.isInFullscreen = null; this.messenger = messenger; } // It is better not to be async handleTopScopeMessages(packet) { (async function() { try { switch (packet.command) { case TopScopeInterface.messages.FULLSCREEN_STATE: this.isInFullscreen = packet.data.isInFullscreen; this.updateFullscreenBtn({ isInFullscreen: this.isInFullscreen }); break; case TopScopeInterface.messages.REPLACE_DOM: await this.replaceDom(packet.data.newDomHref); break; default: break; } } catch (e) { console.error(e); } }.bind(this)()); return { status: 'IframeInterface received a message' }; } async init(player) { this.messenger.registerConnectionListener(this.handleTopScopeMessages.bind(this)); await this.preparePlayer(player); } createSettingsPane() { const pane = new Tweakpane.Pane(); pane.hidden = true; pane.on('change', async () => { await this.messenger.sendMessage(IframeMessenger.messages.UPDATE_SETTINGS); }); GM_addStyle([ // Main container ` .tp-dfwv { --tp-font-family: sans-serif; min-width: 412px; top: 0; right: 0; z-index: 999; } `, // A container one level below the main one ` .tp-rotv { max-height: 420px; font-size: 12px; overflow-y: scroll; scrollbar-width: thin; scrollbar-color: #6b6c73 #37383d; } `, // Any text input ` .tp-txtv_i, .tp-sglv_i { font-size: 14px !important; padding: 0 8px !important; color: var(--in-fg) !important; background-color: var(--in-bg) !important; opacity: 1 !important; } `, ].join(' ')); // Stop leaking events to the player (['keydown', 'keyup', 'keypress'].forEach(event => pane.element.addEventListener(event, (e) => e.stopPropagation()) )); const assignTooltip = (text, object) => (object.element.title = text); const tabs = pane.addTab({ pages: [ { title: 'Preferences' }, { title: 'Advanced' }, ], }); const userTab = tabs.pages[0]; const advancedTab = tabs.pages[1]; const userTabApplyBtn = userTab.addButton({ disabled: true, title: 'Apply', }); const advancedTabApplyBtn = advancedTab.addButton({ disabled: true, title: 'Apply', }); for (const btn of [userTabApplyBtn, advancedTabApplyBtn]) { btn.on('click', () => { setTimeout(() => { userTabApplyBtn.disabled = true; advancedTabApplyBtn.disabled = true; }); }); } pane.element.addEventListener('click', () => { userTabApplyBtn.disabled = false; advancedTabApplyBtn.disabled = false; }); const priorityUserFolder = userTab.addFolder({ title: 'Providers priority' }); (() => { const priorities = userSettings[USER_SETTINGS_LIST.providersPriority]; const buttons = []; priorities.forEach((priority, index) => { const button = priorityUserFolder.addButton({ title: `⬆ ${index + 1}) ${VIDEO_PROVIDERS[priority]}`, }); button.on('click', async () => { if (index > 0) { [priorities[index], priorities[index - 1]] = ( [priorities[index - 1], priorities[index]] ); userSettings[USER_SETTINGS_LIST.providersPriority] = priorities; priorities.forEach((priority, index) => { buttons[index].title = `⬆ ${index + 1}) ${VIDEO_PROVIDERS[priority]}`; }); await this.messenger.sendMessage(IframeMessenger.messages.UPDATE_SETTINGS); } }); buttons.push(button); }); })(); const miscellaneousUserFolder = userTab.addFolder({ title: 'Miscellaneous' }); miscellaneousUserFolder.on('change', (ev) => { if (!ev.last) return; if (typeof ev.value === 'string') { userSettings[ev.presetKey] = userSettings[ev.presetKey].trim(); ev.target.refresh(); } }); assignTooltip(( 'Amount of seconds to skip or rewind by pressing corresponding hotkeys' ), miscellaneousUserFolder.addInput(userSettings, USER_SETTINGS_LIST.fastForwardSizeS, { step: 1, min: 0, label: 'Fast forward size, sec', }, )); assignTooltip(( 'Large skip size when pressing the corresponding hotkey' ), miscellaneousUserFolder.addInput(userSettings, USER_SETTINGS_LIST.largeSkipSizeS, { step: 1, min: 0, label: 'Large skip size, sec', }, )); assignTooltip(( 'Amount of seconds of approximate playback time after which a video is being marked as seen. Set to 0 to disable and mark only by a triggered autoplay' ), miscellaneousUserFolder.addInput(userSettings, USER_SETTINGS_LIST.markWatchedAfterS, { step: 1, min: 0, label: 'Mark watched after, sec', }, )); assignTooltip(( 'Automatically go to next video when video player has got into this amount of seconds left to play' ), miscellaneousUserFolder.addInput(userSettings, USER_SETTINGS_LIST.outroSkipThresholdS, { step: 1, min: 0.5, label: 'Outro skip threshold, sec', }, )); assignTooltip(( 'If this is 0, outro skip is being applied throughout all of the THRESHOLD seconds amount, so when user seeks to the end of the video, it is being skipped. If this is more than 0, skip would be applied only from THRESHOLD to (THRESHOLD minus THRESHOLD SPREAD). Example: if a video is 2 minutes long, THRESHOLD is 20 and THRESHOLD SPREAD is 5, outro skip might be triggered only from 1:40 to 1:45' ), miscellaneousUserFolder.addInput(userSettings, USER_SETTINGS_LIST.outroSkipThresholdSpreadS, { step: 1, min: 0, label: 'Outro skip threshold spread, sec', }, )); miscellaneousUserFolder.addButton({ title: 'Reset to defaults', }).on('click', () => { userSettings.update(USER_SETTINGS_DEFAULTS); pane.refresh(); }); const hotkeysUserFolder = userTab.addFolder({ title: 'Hotkeys' }); hotkeysUserFolder.on('change', (ev) => { if (!ev.last) return; if (typeof ev.value === 'string') { hotkeysSettings[ev.presetKey] = hotkeysSettings[ev.presetKey].trim(); ev.target.refresh(); } }); assignTooltip(( 'Hotkey for a fast backward. A page reload is required for this setting to take effect!' ), hotkeysUserFolder.addInput(hotkeysSettings, HOTKEYS_SETTINGS_LIST.fastBackward, { label: 'Fast backward*', }, )); assignTooltip(( 'Hotkey for a fast forward. A page reload is required for this setting to take effect!' ), hotkeysUserFolder.addInput(hotkeysSettings, HOTKEYS_SETTINGS_LIST.fastForward, { label: 'Fast forward*', }, )); assignTooltip(( 'Hotkey for a fullscreen mode toggle. A page reload is required for this setting to take effect!' ), hotkeysUserFolder.addInput(hotkeysSettings, HOTKEYS_SETTINGS_LIST.fullscreen, { label: 'Fullscreen*', }, )); assignTooltip(( 'Hotkey for a large fast forward, typically on intros. A page reload is required for this setting to take effect!' ), hotkeysUserFolder.addInput(hotkeysSettings, HOTKEYS_SETTINGS_LIST.largeSkip, { label: 'Large skip*', }, )); const howToHotkeysUserBtn = hotkeysUserFolder.addButton({ title: 'Hotkeys guide' }); howToHotkeysUserBtn.on('click', async () => { await this.messenger.sendMessage(IframeMessenger.messages.OPEN_HOTKEYS_GUIDE); }); hotkeysUserFolder.addButton({ title: 'Reset to defaults', }).on('click', () => { hotkeysSettings.update(HOTKEYS_SETTINGS_DEFAULTS); pane.refresh(); }); const miscellaneousCoreFolder = advancedTab.addFolder({ title: 'Miscellaneous' }); miscellaneousCoreFolder.on('change', (ev) => { if (!ev.last) return; if (typeof ev.value === 'string') { coreSettings[ev.presetKey] = coreSettings[ev.presetKey].trim(); ev.target.refresh(); } }); assignTooltip(( 'Since all videos are being hosted at a different domain, the script can not request videos data without a bypass. One of a bypasses triggers script manager warning about requests to a different domain, so instead the script uses another bypass which passes some requests through its own server' ), miscellaneousCoreFolder.addInput(coreSettings, CORE_SETTINGS_LIST.corsProxy, { label: 'CORS proxy', }, )); assignTooltip(( 'Reflects messaging responsiveness between a player and a top scope. Might impact CPU usage if set too low. 40 should be enough. A page reload is required for this setting to take effect!' ), miscellaneousCoreFolder.addInput(coreSettings, CORE_SETTINGS_LIST.commlinkPollingIntervalMs, { step: 10, min: 10, max: 500, label: 'Commlink polling interval, ms*', }, )); assignTooltip(( 'Push some console logs. A page reload is required for this setting to take effect!' ), miscellaneousCoreFolder.addInput(coreSettings, CORE_SETTINGS_LIST.debug, { label: 'Debug*', }, )); assignTooltip(( 'Large skip hotkey also starts playback' ), miscellaneousCoreFolder.addInput(coreSettings, CORE_SETTINGS_LIST.playOnLargeSkip, { label: 'Play on large skip', }, )); assignTooltip(( 'Cooldown for a large skip hotkey, to prevent an accidental double skip. A page reload is required for this setting to take effect!' ), miscellaneousCoreFolder.addInput(coreSettings, CORE_SETTINGS_LIST.largeSkipCooldownMs, { step: 1, min: 0, label: 'Large skip cooldown, ms*', }, )); miscellaneousCoreFolder.addButton({ title: 'Reset to defaults', }).on('click', () => { coreSettings.update(CORE_SETTINGS_DEFAULTS); pane.refresh(); }); return pane; } async replaceDom(newDomHref) { const corsProxy = coreSettings[CORE_SETTINGS_LIST.corsProxy]; const newDomHtml = await (await fetch(corsProxy + newDomHref)).text(); if (!newDomHref || !newDomHtml) throw new Error('Something went wrong'); // Makes possible to automatically press play // Doesn't make possible to automatically go fullscreen for some reason document.open(); document.write(newDomHtml); document.close(); } } class VOEIframeInterface extends IframeInterface { constructor(messenger) { super(messenger); const originalAddEventListener = EventTarget.prototype.addEventListener; // Intercept original hotkeys in order to prevent a wrong fullscreen EventTarget.prototype.addEventListener = function (type, listener, options) { if (type === 'keydown' && this.matches && this.matches('div.plyr')) return; return originalAddEventListener.call(this, type, listener, options); }; waitForNode('iframe[style*="z-index: 2147483647"]', { existing: true, onceOnly: true, }, (ads) => ads.remove()); } static queries = { fullscreenBtn: 'button[data-plyr="fullscreen"]', player: 'video#voe-player', playerContainer: 'div.plyr__video-wrapper', playerControls: 'div.plyr__controls', }; createAutoplayButton() { const button = document.createElement('button'); const toggleContainer = document.createElement('div'); const toggleDot = document.createElement('div'); const tooltip = document.createElement('span'); const isAutoplayEnabled = userSettings[USER_SETTINGS_LIST.isAutoplayEnabled]; button.addEventListener('click', () => { const wasEnabled = userSettings[USER_SETTINGS_LIST.isAutoplayEnabled]; userSettings[USER_SETTINGS_LIST.isAutoplayEnabled] = !wasEnabled; button.setAttribute('aria-checked', (!wasEnabled).toString()); tooltip.textContent = wasEnabled ? 'Autoplay is disabled' : 'Autoplay is enabled'; toggleDot.style.backgroundColor = wasEnabled ? '#bbb' : '#fff'; toggleDot.style.transform = wasEnabled ? 'translateX(0px)' : 'translateX(12px)'; }); button.type = 'button'; button.appendChild(toggleContainer); button.appendChild(tooltip); button.setAttribute('aria-checked', (isAutoplayEnabled).toString()); button.className = ( 'plyr__controls__item plyr__control Autoplay-button' ); toggleContainer.className = 'Autoplay-button--toggle'; toggleContainer.appendChild(toggleDot); toggleDot.className = 'Autoplay-button--toggle-dot'; tooltip.className = 'plyr__tooltip'; tooltip.textContent = ( !isAutoplayEnabled ? 'Autoplay is disabled' : 'Autoplay is enabled' ); toggleDot.style.backgroundColor = !isAutoplayEnabled ? '#bbb' : '#fff'; toggleDot.style.transform = ( !isAutoplayEnabled ? 'translateX(0px)' : 'translateX(12px)' ); GM_addStyle([` .Autoplay-button { width: 36px !important; height: 36px; padding: 0 !important; background-color: #bbb; border-radius: 50%; position: absolute; top: 0; left: 0; transition: all 0.2s ease; } .Autoplay-button[aria-checked="true"] .Autoplay-button--toggle-dot { transform: translateX(12px); } .Autoplay-button:hover .plyr__tooltip { opacity: 1; } .Autoplay-button--toggle { width: 24px; height: 12px; vertical-align: -1px; background-color: rgba(221, 221, 221, 0.5); border-radius: 6px; position: relative; cursor: pointer; display: inline-block; } .Autoplay-button--toggle-dot { width: 12px; height: 12px; background-color: #bbb; border-radius: 50%; position: absolute; top: 0; left: 0; transition: all 0.2s ease; } `][0]); return button; } async preparePlayer(player) { if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastForward]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastForward], () => { if (userSettings[USER_SETTINGS_LIST.fastForwardSizeS]) { player.plyr.currentTime += userSettings[USER_SETTINGS_LIST.fastForwardSizeS]; } }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastBackward]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastBackward], () => { if (userSettings[USER_SETTINGS_LIST.fastForwardSizeS]) { player.plyr.currentTime -= userSettings[USER_SETTINGS_LIST.fastForwardSizeS]; } }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fullscreen]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fullscreen], async (ev) => { ev.preventRepeat(); await this.messenger.sendMessage(IframeMessenger.messages.TOGGLE_FULLSCREEN); }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.largeSkip]) { const cooldownTime = coreSettings[CORE_SETTINGS_LIST.largeSkipCooldownMs]; let lastSkipTime = 0; keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.largeSkip], () => { if (userSettings[USER_SETTINGS_LIST.largeSkipSizeS]) { const now = Date.now(); if (now - lastSkipTime < cooldownTime) return; lastSkipTime = now; player.plyr.currentTime += userSettings[USER_SETTINGS_LIST.largeSkipSizeS]; } if (coreSettings[CORE_SETTINGS_LIST.playOnLargeSkip]) { player.plyr.play(); } }); } // Hide cursor on a fullscreen playback waitForNode(VOEIframeInterface.queries.playerContainer, { existing: true, onceOnly: true, }, (playerContainer) => { let lastMove = Date.now(); playerContainer.addEventListener('mousemove', () => { lastMove = Date.now(); playerContainer.style.cursor = 'default'; }); setInterval(() => { if ( this.isInFullscreen && player.plyr.playing && (Date.now() - lastMove > 2000) ) { playerContainer.style.cursor = 'none'; } }, 100); }); // Attach autoplay button and change fullscreen button behavior... waitForNode(VOEIframeInterface.queries.playerControls, { existing: true, onceOnly: true, }, (playerControls) => { const fsBtn = playerControls.querySelector(VOEIframeInterface.queries.fullscreenBtn); const newFsBtn = fsBtn.cloneNode(true); const autoplayBtn = this.createAutoplayButton(); const settingsPane = this.createSettingsPane(); fsBtn.before(autoplayBtn); fsBtn.replaceWith(newFsBtn); autoplayBtn.oncontextmenu = () => { settingsPane.hidden = !settingsPane.hidden; return false; }; newFsBtn.addEventListener('click', async () => { await this.messenger.sendMessage(IframeMessenger.messages.TOGGLE_FULLSCREEN); }); }); await (async function waitForPlyr() { if (player.plyr === undefined) { await sleep(50); return waitForPlyr(); } }()); if (userSettings[USER_SETTINGS_LIST.isAutoplayEnabled]) player.plyr.play(); // Change doubleclick-to-fullscreen feature behavior for (let i = player.plyr.eventListeners.length - 1; i >= 0; i--) { const listenerObject = player.plyr.eventListeners[i]; if (listenerObject.type === 'dblclick') { listenerObject.element.removeEventListener('dblclick', listenerObject.callback); player.plyr.eventListeners.splice(i, 1); listenerObject.element.addEventListener('dblclick', async () => { await this.messenger.sendMessage(IframeMessenger.messages.TOGGLE_FULLSCREEN); }); break; } } // Handle "seen" state and an outro skip player.plyr.on('timeupdate', (() => { let approximatePlayTime = 0; let currentVideoWasWatched = false; let outroHasBeenReached = false; return async () => { // 250ms is approximate interval in between this event calls approximatePlayTime += 0.25; if ( !currentVideoWasWatched && userSettings[USER_SETTINGS_LIST.markWatchedAfterS] && (approximatePlayTime >= userSettings[USER_SETTINGS_LIST.markWatchedAfterS]) ) { currentVideoWasWatched = true; try { await this.messenger.sendMessage(IframeMessenger.messages.MARK_CURRENT_VIDEO_SEEN); } catch (e) { console.error(e); } } if ( outroHasBeenReached || !userSettings[USER_SETTINGS_LIST.isAutoplayEnabled] ) return; const timeLeft = player.duration - player.currentTime; const skipWindow = ( userSettings[USER_SETTINGS_LIST.outroSkipThresholdS] - userSettings[USER_SETTINGS_LIST.outroSkipThresholdSpreadS] ); if (userSettings[USER_SETTINGS_LIST.outroSkipThresholdSpreadS] === 0) { if (timeLeft <= userSettings[USER_SETTINGS_LIST.outroSkipThresholdS]) { outroHasBeenReached = true; } } else { if ( timeLeft >= skipWindow && timeLeft <= userSettings[USER_SETTINGS_LIST.outroSkipThresholdS] ) { outroHasBeenReached = true; } } if (outroHasBeenReached) { await this.messenger.sendMessage(IframeMessenger.messages.AUTOPLAY_NEXT, { currentVideoId: player.src, }); } }; })()); await this.messenger.sendMessage(IframeMessenger.messages.GET_FULLSCREEN_STATE); } updateFullscreenBtn({ isInFullscreen }) { const btn = document.querySelector(VOEIframeInterface.queries.fullscreenBtn); if (isInFullscreen) { btn.classList.add('plyr__control--pressed'); } else { btn.classList.remove('plyr__control--pressed'); } } } class VidozaIframeInterface extends IframeInterface { constructor(messenger) { super(messenger); waitForNode('iframe[style*="z-index: 2147483647"]', { existing: true, }, (ads) => ads.remove()); } static queries = { fullscreenBtn: 'button.vjs-fullscreen-control', player: 'video#player_html5_api.vjs-tech', }; createAutoplayButton() { const button = document.createElement('button'); const toggleContainer = document.createElement('div'); const toggleDot = document.createElement('div'); const isAutoplayEnabled = userSettings[USER_SETTINGS_LIST.isAutoplayEnabled]; button.addEventListener('click', () => { const wasEnabled = userSettings[USER_SETTINGS_LIST.isAutoplayEnabled]; userSettings[USER_SETTINGS_LIST.isAutoplayEnabled] = !wasEnabled; button.setAttribute('aria-checked', (!wasEnabled).toString()); button.title = ( !isAutoplayEnabled ? 'Autoplay is disabled' : 'Autoplay is enabled' ); toggleDot.style.backgroundColor = wasEnabled ? '#e1e1e1' : '#fff'; toggleDot.style.transform = wasEnabled ? 'translateX(0px)' : 'translateX(12px)'; }); button.type = 'button'; button.title = ( !isAutoplayEnabled ? 'Autoplay is disabled' : 'Autoplay is enabled' ); button.appendChild(toggleContainer); button.setAttribute('aria-checked', (isAutoplayEnabled).toString()); button.className = 'Autoplay-button'; toggleContainer.className = 'Autoplay-button--toggle'; toggleContainer.appendChild(toggleDot); toggleDot.className = 'Autoplay-button--toggle-dot'; toggleDot.style.backgroundColor = !isAutoplayEnabled ? '#e1e1e1' : '#fff'; toggleDot.style.transform = ( !isAutoplayEnabled ? 'translateX(0px)' : 'translateX(12px)' ); GM_addStyle([` .Autoplay-button { width: 36px !important; height: 36px; padding: 0 !important; border-radius: 50%; top: 0; left: 0; transition: all 0.2s ease; } .Autoplay-button[aria-checked="true"] .Autoplay-button--toggle-dot { transform: translateX(12px); } .Autoplay-button--toggle { width: 24px; height: 12px; margin-bottom: 3px; background-color: rgba(221, 221, 221, 0.5); border-radius: 6px; position: relative; cursor: pointer; display: inline-block; } .Autoplay-button--toggle-dot { width: 12px; height: 12px; background-color: #e1e1e1; border-radius: 50%; position: absolute; top: 0; left: 0; transition: all 0.2s ease; } `][0]); return button; } async preparePlayer(player) { if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastForward]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastForward], () => { if (userSettings[USER_SETTINGS_LIST.fastForwardSizeS]) { player.currentTime += userSettings[USER_SETTINGS_LIST.fastForwardSizeS]; } }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastBackward]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fastBackward], () => { if (userSettings[USER_SETTINGS_LIST.fastForwardSizeS]) { player.currentTime -= userSettings[USER_SETTINGS_LIST.fastForwardSizeS]; } }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.fullscreen]) { keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.fullscreen], async (ev) => { ev.preventRepeat(); await this.messenger.sendMessage(IframeMessenger.messages.TOGGLE_FULLSCREEN); }); } if (hotkeysSettings[HOTKEYS_SETTINGS_LIST.largeSkip]) { const cooldownTime = coreSettings[CORE_SETTINGS_LIST.largeSkipCooldownMs]; let lastSkipTime = 0; keyboardJS.bind(hotkeysSettings[HOTKEYS_SETTINGS_LIST.largeSkip], () => { if (userSettings[USER_SETTINGS_LIST.largeSkipSizeS]) { const now = Date.now(); if (now - lastSkipTime < cooldownTime) return; lastSkipTime = now; player.currentTime += userSettings[USER_SETTINGS_LIST.largeSkipSizeS]; if (coreSettings[CORE_SETTINGS_LIST.playOnLargeSkip]) { player.play(); } } }); } player.volume = coreSettings[CORE_SETTINGS_LIST.vidozaPersistentVolumeLvl]; player.addEventListener('volumechange', () => { coreSettings[CORE_SETTINGS_LIST.vidozaPersistentVolumeLvl] = player.volume; }); // Attach autoplay button and change fullscreen button behavior... waitForNode(VidozaIframeInterface.queries.fullscreenBtn, { existing: true, onceOnly: true, }, (fsBtn) => { const newFsBtn = fsBtn.cloneNode(true); const autoplayBtn = this.createAutoplayButton(); const settingsPane = this.createSettingsPane(); fsBtn.before(autoplayBtn); fsBtn.replaceWith(newFsBtn); autoplayBtn.oncontextmenu = () => { settingsPane.hidden = !settingsPane.hidden; return false; }; newFsBtn.addEventListener('click', async () => { await this.messenger.sendMessage(IframeMessenger.messages.TOGGLE_FULLSCREEN); }); }); // Handle "seen" state and an outro skip player.addEventListener('timeupdate', (() => { let approximatePlayTime = 0; let currentVideoWasWatched = false; let outroHasBeenReached = false; return async () => { // 265ms is approximate interval in between this event calls approximatePlayTime += 0.265; if ( !currentVideoWasWatched && userSettings[USER_SETTINGS_LIST.markWatchedAfterS] && (approximatePlayTime >= userSettings[USER_SETTINGS_LIST.markWatchedAfterS]) ) { currentVideoWasWatched = true; try { await this.messenger.sendMessage(IframeMessenger.messages.MARK_CURRENT_VIDEO_SEEN); } catch (e) { console.error(e); } } if ( outroHasBeenReached || !userSettings[USER_SETTINGS_LIST.isAutoplayEnabled] ) return; const timeLeft = player.duration - player.currentTime; const skipWindow = ( userSettings[USER_SETTINGS_LIST.outroSkipThresholdS] - userSettings[USER_SETTINGS_LIST.outroSkipThresholdSpreadS] ); if (userSettings[USER_SETTINGS_LIST.outroSkipThresholdSpreadS] === 0) { if (timeLeft <= userSettings[USER_SETTINGS_LIST.outroSkipThresholdS]) { outroHasBeenReached = true; } } else { if ( timeLeft >= skipWindow && timeLeft <= userSettings[USER_SETTINGS_LIST.outroSkipThresholdS] ) { outroHasBeenReached = true; } } if (outroHasBeenReached) { await this.messenger.sendMessage(IframeMessenger.messages.AUTOPLAY_NEXT, { currentVideoId: player.src, }); } }; })()); await this.messenger.sendMessage(IframeMessenger.messages.GET_FULLSCREEN_STATE); } updateFullscreenBtn({ isInFullscreen }) { const player = document.querySelector(VidozaIframeInterface.queries.player); if (isInFullscreen) { player.parentElement.classList.add('vjs-fullscreen'); } else { player.parentElement.classList.remove('vjs-fullscreen'); } } } class TopScopeInterface { constructor() { this.commLink = null; this.currentIframeId = null; this.currentIframeProvider = null; this.id = makeId(); this.isPendingConnection = false; this.prevIframeVideoId = null; } static messages = { FULLSCREEN_STATE: 'FULLSCREEN_STATE', REPLACE_DOM: 'REPLACE_DOM', }; static queries = { animeTitle: 'div.hostSeriesTitle', episodeDedicatedLink: 'div.hosterSiteVideo a.watchEpisode', episodeTitle: 'div.hosterSiteTitle', hostersPlayerContainer: 'div.hosterSiteVideo', navLinksContainer: 'div#stream.hosterSiteDirectNav', playerIframe: 'div.inSiteWebStream iframe', providerName: 'div.hosterSiteVideo > ul a > h4', providersList: 'div.hosterSiteVideo > ul', }; // It is better not to be async handleIframeMessages(packet) { const receivingResult = { status: 'TopScopeInterface received a message', }; (async function() { try { switch (packet.command) { case IframeMessenger.messages.AUTOPLAY_NEXT: // This is here because it bugges out the episodes navigation panel // if try and use MARK_CURRENT_VIDEO_SEEN. Seen episode is being // marked as non seen try { await this.markCurrentVideoSeen(); } catch (e) { console.error(e); } const prevProviderName = this.currentIframeProvider; await this.goToNextVideo({ currentIframeVideoId: packet.data.currentVideoId, }); // Since everything except VOE-to-VOE is being done // by replacing the iframe src, initCrossFrameConnection() // is going to be called by src change mutation observer. // Thus need to call initCrossFrameConnection() here only // when REPLACE_DOM is happening if ( prevProviderName === 'VOE' && this.currentIframeProvider === 'VOE' ) { this.unregisterCommlinkListener(); await this.initCrossFrameConnection(); } break; case IframeMessenger.messages.GET_FULLSCREEN_STATE: await this.commLink.commands[TopScopeInterface.messages.FULLSCREEN_STATE]({ isInFullscreen: !!document.fullscreenElement, }); break; case IframeMessenger.messages.MARK_CURRENT_VIDEO_SEEN: await this.markCurrentVideoSeen(); break; case IframeMessenger.messages.OPEN_HOTKEYS_GUIDE: if (confirm('Open hotkeys guide?')) { window.open('https://i.imgur.com/3cnDWxm.png', '_blank'); } break; case IframeMessenger.messages.TOGGLE_FULLSCREEN: // Notice how this then triggers a listener from this.init() if (document.fullscreenElement) { await document.exitFullscreen(); } else { await document.documentElement.requestFullscreen(); } break; case IframeMessenger.messages.UPDATE_SETTINGS: coreSettings.update(); hotkeysSettings.update(); userSettings.update(); break; default: break; } } catch (e) { console.error(e); } }.bind(this)()); return receivingResult; } async init() { await this.initCrossFrameConnection(); document.addEventListener('fullscreenchange', async () => { this.toggleFakeFullscreen(); await this.commLink.commands[TopScopeInterface.messages.FULLSCREEN_STATE]({ isInFullscreen: !!document.fullscreenElement, }); }); } async initCrossFrameConnection() { // Just in case if (this.isPendingConnection) throw new Error('Connecting already'); this.isPendingConnection = true; const iframeId = this.currentIframeId = await new Promise((resolve) => { const valueChangeListenerId = GM_addValueChangeListener('unboundIframeData', ( _key, _oldValue, newValue, ) => { const iframe = document.querySelector(TopScopeInterface.queries.playerIframe); const { id: iframeId, width: iframeScopeWidth, height: iframeScopeHeight, } = newValue; // Skip if it is a wrong iframe, judging by its size. // Alternatively I can use domains blacklist if ( !iframe || (iframeScopeWidth !== iframe.offsetWidth) || (iframeScopeHeight !== iframe.offsetHeight) ) return; GM_removeValueChangeListener(valueChangeListenerId); resolve(iframeId); }); }); GM_setValue(iframeId, this.id); this.commLink = new CommLinkHandler(this.id, { silentMode: !coreSettings[CORE_SETTINGS_LIST.debug], statusCheckInterval: coreSettings[CORE_SETTINGS_LIST.commlinkPollingIntervalMs], }); this.commLink.registerSendCommand(TopScopeInterface.messages.FULLSCREEN_STATE); this.commLink.registerSendCommand(TopScopeInterface.messages.REPLACE_DOM); this.commLink.registerListener(iframeId, this.handleIframeMessages.bind(this)); this.isPendingConnection = false; } async announceEpisodeWatched(id) { const url = `${location.protocol}//${location.hostname}/ajax/watchEpisode`; const options = { method: 'POST', body: `episode=${id}`, headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', }, }; const reqResult = await fetch(url, options).then(res => res.json()); if (reqResult.status === false) await fetch(url, options); } async markCurrentVideoSeen() { const episodeTitle = document.querySelector(TopScopeInterface.queries.episodeTitle); const { episodeId } = episodeTitle.dataset; await this.announceEpisodeWatched(episodeId); } toggleFakeFullscreen() { const Q = TopScopeInterface.queries; const isInFullscreen = !!document.fullscreenElement; const hostersPlayerContainer = document.querySelector(Q.hostersPlayerContainer); const playerIframe = document.querySelector(Q.playerIframe); if (isInFullscreen) { document.body.style.overflow = 'hidden'; playerIframe.style.height = '100vh'; hostersPlayerContainer.firstElementChild.style.display = 'none'; hostersPlayerContainer.style.cssText = ( 'z-index: 100; position: fixed; top: 0; left: 0; padding: 0; height: 100vh; overflow-y: scroll; scrollbar-width: none;' ); } else { document.body.style.overflow = ''; playerIframe.style.height = ''; // scrollTop reset must go before the cssText, it won't work otherwise hostersPlayerContainer.firstElementChild.style.display = ''; hostersPlayerContainer.scrollTop = 0; hostersPlayerContainer.style.cssText = ''; } } async goToNextVideo({ currentIframeVideoId }) { const Q = TopScopeInterface.queries; // Ignore possible goToNextVideo flood spam if (this.prevIframeVideoId === currentIframeVideoId) return; this.prevIframeVideoId = currentIframeVideoId; const [seasonsNav, episodesNav] = document.querySelectorAll(`${Q.navLinksContainer} > ul`); const episodesNavLinks = [...episodesNav.querySelectorAll('a')]; const seasonNavLinks = [...seasonsNav.querySelectorAll('a')]; const currentEpisodeIndex = episodesNavLinks.findIndex(el => el.classList.contains('active')); const currentSeasonIndex = seasonNavLinks.findIndex(el => el.classList.contains('active')); let nextEpisodeHref = null; if (currentEpisodeIndex < episodesNavLinks.length - 1) { nextEpisodeHref = episodesNavLinks[currentEpisodeIndex + 1].href; } else if (currentSeasonIndex < seasonNavLinks.length - 1) { // Do not proceed if this is a last movie // so it wont hop in to a season from a movie if (seasonNavLinks[currentSeasonIndex].href.endsWith('/filme')) return; const nextSeasonHref = seasonNavLinks[currentSeasonIndex + 1].href; const nextSeasonHtml = await (await fetch(nextSeasonHref)).text(); const nextSeasonDom = (new DOMParser()).parseFromString(nextSeasonHtml, 'text/html'); const firstEpisodeLink = nextSeasonDom.querySelector( `${Q.navLinksContainer} > ul a[data-episode-id]` ); nextEpisodeHref = firstEpisodeLink.href; } // Skip cause it seems like it is a very last episode if (!nextEpisodeHref) return; const nextEpisodeHtml = await (await fetch(nextEpisodeHref)).text(); const nextEpisodeDom = (new DOMParser()).parseFromString(nextEpisodeHtml, 'text/html'); const providersList = nextEpisodeDom.querySelector(Q.providersList); const nextVideoLinks = providersList.querySelectorAll(Q.episodeDedicatedLink); let nextProviderName = null; let nextVideoLink = null; outer: for (const priority of userSettings[USER_SETTINGS_LIST.providersPriority]) { const preferredProviderName = VIDEO_PROVIDERS[priority]; for (const link of nextVideoLinks) { const providerName = link.querySelector(Q.providerName).innerText; if (providerName === preferredProviderName) { nextProviderName = providerName; nextVideoLink = link; break outer; } } } if (!nextVideoLink.href) throw new Error('Unable to get next video link'); // This check also happens at handleIframeMessages() if ( nextProviderName === 'VOE' && this.currentIframeProvider === 'VOE' ) { await this.commLink.commands[TopScopeInterface.messages.REPLACE_DOM]({ newDomHref: nextVideoLink.href, }); } else { document.querySelector(Q.playerIframe).src = nextVideoLink.href; } this.currentIframeProvider = nextProviderName; // Update current DOM from a next episode DOM [ 'div#wrapper > div.seriesContentBox > div.container.marginBottom > ul', 'div#wrapper > div.seriesContentBox > div.container.marginBottom > div.cf', `${Q.episodeTitle} > ul`, Q.animeTitle, Q.episodeTitle, Q.navLinksContainer, Q.providersList, ].forEach((query) => { const currentElement = document.querySelector(query); const newElement = nextEpisodeDom.querySelector(query); if (currentElement && newElement) { currentElement.outerHTML = newElement.outerHTML; } }); document.title = nextEpisodeDom.title; history.pushState({}, '', nextEpisodeHref); // The website code copypasta document.querySelectorAll('.generateInlinePlayer').forEach((btn) => { btn.addEventListener('click', (ev) => { ev.preventDefault(); const parent = btn.parentElement; const linkTarget = parent.getAttribute('data-link-target'); const hosterTarget = parent.getAttribute('data-external-embed') === 'true'; const fakePlayer = document.querySelector('.fakePlayer'); const inSiteWebStream = document.querySelector('.inSiteWebStream'); const iframe = inSiteWebStream.querySelector('iframe'); if (hosterTarget) { fakePlayer.style.display = 'block'; inSiteWebStream.style.display = 'inline-block'; iframe.style.display = 'none'; } else { fakePlayer.style.display = 'none'; inSiteWebStream.style.display = 'inline-block'; iframe.src = linkTarget; iframe.style.display = 'inline-block'; } }); }); } unregisterCommlinkListener() { if (!this.currentIframeId) return; this.commLink.listeners = this.commLink.listeners.filter((listener) => { if (listener.sender === this.currentIframeId) { listener.intervalObj.stop(); return false; } return true; }); this.currentIframeId = null; } } if (window.self === window.top) { if (TOP_SCOPE_DOMAINS.includes(location.hostname)) { waitForNode(TopScopeInterface.queries.playerIframe, { existing: true, onceOnly: true, }, async (iframe) => { const topScopeInterface = new TopScopeInterface(); new Promise((resolve) => { if (['complete'].includes(document.readyState)) { resolve(); } else { document.addEventListener('DOMContentLoaded', resolve, { once: true }); } }).then(() => { const list = document.querySelector(TopScopeInterface.queries.providersList); const links = list.querySelectorAll(TopScopeInterface.queries.episodeDedicatedLink); outer: for (const priority of userSettings[USER_SETTINGS_LIST.providersPriority]) { const preferredProviderName = VIDEO_PROVIDERS[priority]; for (const link of links) { const providerName = link.querySelector( TopScopeInterface.queries.providerName ).innerText; if (providerName === preferredProviderName) { topScopeInterface.currentIframeProvider = providerName; setTimeout(() => link.parentElement.click()); break outer; } } } }); await topScopeInterface.init(); new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'src') { topScopeInterface.unregisterCommlinkListener(); topScopeInterface.initCrossFrameConnection(); } }); }).observe(iframe, { attributes: true }); }); } } else { const iframeMessenger = new IframeMessenger(); await iframeMessenger.initCrossFrameConnection(); waitForNode([ VOEIframeInterface.queries.player, VidozaIframeInterface.queries.player, ].join(', '), { existing: true, onceOnly: true, }, async (player) => { if (player.matches(VOEIframeInterface.queries.player)) { await (new VOEIframeInterface(iframeMessenger)).init(player); } else if (player.matches(VidozaIframeInterface.queries.player)) { await (new VidozaIframeInterface(iframeMessenger)).init(player); } }); } }());
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址