// ==UserScript== // @name BETSLIX - burning series enhancer // @name:en BETSLIX - burning series enhancer // @icon https://bs.to/favicon.ico // @author xtrars // @description:de Wechselt automatisch zum VOE- oder Streamtape-Tab auf burning series und öffnet VOE oder Streamtape. Das Tool startet das nächste Video und falls nötig die nächste Staffel, wenn eine Episode beendet wurde. // @description:en Automatically switches to the VOE or Streamtape tab on burning series and opens VOE or Streamtape. The tool starts the next video and if necessary the next season when an episode is finished. // @version 16.5 // @run-at document-start // @license GPL-3.0-or-later // @namespace https://greasyfork.org/users/140785 // @compatible chrome Chrome // @compatible firefox Firefox // @compatible opera Opera // @compatible edge Edge // @compatible safari Safari // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_download // @grant GM_info // @grant GM_addStyle // @grant GM_getResourceText // @grant window.close // @grant window.focus // @match https://bs.to/* // @match https://burningseries.co/* // @match https://burningseries.sx/* // @match https://burningseries.vc/* // @match https://burningseries.ac/* // @match https://burningseries.cx/* // @match https://burningseries.nz/* // @match https://burningseries.se/* // @match https://dood.yt/* // @match https://d0000d.com/* // @match https://streamtape.com/* // @match https://streamadblocker.xyz/* // @match https://*.tapecontent.net/* // @match https://*.vidoza.net/* // @match https://*.videzz.net/* // @match https://*.voe-network.net/* // @match https://voe.sx/* // @match https://*.richardsignfish.com/* // @require https://unpkg.com/video.js@latest/dist/video.min.js // @require https://unpkg.com/hls.js@latest/dist/hls.min.js // @description Wechselt automatisch zum VOE- oder Streamtape-Tab auf burning series und öffnet VOE oder Streamtape. Das Tool startet das nächste Video und falls nötig die nächste Staffel, wenn eine Episode beendet wurde. // @downloadURL https://update.greasyfork.icu/scripts/429666/BETSLIX%20-%20burning%20series%20enhancer.user.js // @updateURL https://update.greasyfork.icu/scripts/429666/BETSLIX%20-%20burning%20series%20enhancer.meta.js // ==/UserScript== /** * The CBaseHandler class provides basic helper functions for handling DOM elements and URLs. * @class */ class CBaseHandler { /** * Returns the hoster as a string * @param {number} iIndex - 0: VOE, 1: Streamtape, 2: Doodstream, 3: Vidoza * @param {boolean} bAllLowerCase - If true, returns the hoster name in all lowercase * @returns {string} - The hoster name */ getHoster(iIndex, bAllLowerCase = false) { const aHoster = ['VOE', 'Streamtape', 'Doodstream', 'Vidoza']; return bAllLowerCase ? aHoster[iIndex].toLowerCase() : aHoster[iIndex]; } /** * Waits for an element to be available in the DOM and resolves with the element * @param {string} sSelector - The CSS selector of the element to wait for * @param {boolean} bWaitUnlimited - If true, waits indefinitely for the element. If false, waits for a maximum of 3 seconds * @returns {Promise} - A promise that resolves with the element when it becomes available in the DOM */ waitForElement(sSelector, bWaitUnlimited = true) { return new Promise(async resolve => { if (document.querySelector(sSelector)) { return resolve(document.querySelector(sSelector)); } const oObserver = new MutationObserver(() => { if (document.querySelector(sSelector)) { resolve(document.querySelector(sSelector)); oObserver.disconnect(); } }); if (document.body) { oObserver.observe(document.body, { childList: true, subtree: true, }); } if (!bWaitUnlimited) { setTimeout(() => { resolve(document.querySelector(sSelector)); oObserver.disconnect(); }, 3000); } }); } /** * Checks if the current URL contains all the selectors in the given array * @param {Array} aSelector - An array of URL selectors to check * @returns {boolean} - True if the URL contains all the selectors, false otherwise */ hasUrl(aSelector) { let bIsAvailable = true; for (let rSelector of aSelector) { bIsAvailable = document.location.href.search(rSelector) !== -1; if (!bIsAvailable) { return false; } } return true; } /** * Reloads the current page after the specified delay * @param {number} iDelay - The delay in milliseconds before reloading the page */ reload(iDelay = 300) { setTimeout(() => { window.location.reload(); }, iDelay); } /** * Returns the first element matching the given CSS selector and attribute that matches the specified regular expression * @param {string} sSelector - The CSS selector to search for * @param {string} sAttribute - The attribute to match the regular expression against * @param {RegExp} rRegex - The regular expression to match against the attribute value * @returns {HTMLElement|boolean} - The first matching element, or false if no matching element is found */ querySelectorAllRegex(sSelector = '*', sAttribute = 'name', rRegex = /.*/) { for (const oElement of document.querySelectorAll(sSelector)) { if (rRegex.test(oElement[sAttribute])) { return oElement; } } return false; } restartAnimation(oEl) { oEl.style.animation = 'none'; oEl.offsetHeight; /* trigger reflow */ oEl.style.animation = null; } } /** The CBurningSeriesHandler class extends the CBaseHandler class and provides methods for handling and enhancing the Burning Series website. This class is responsible for building the settings window that is displayed when the settings button is clicked. @class @extends CBaseHandler */ class CBurningSeriesHandler extends CBaseHandler { /** Initializes the values for the user script's settings. */ initGMVariables() { const oVariables = [ {name: 'bActivateEnhancer', defaultValue: false}, {name: 'bAutoplayNextSeason', defaultValue: true}, {name: 'bAutoplayRandomEpisode', defaultValue: false}, {name: 'bSelectHoster', defaultValue: this.getHoster(0, true)}, {name: 'bSkipStart', defaultValue: false}, {name: 'bSkipEnd', defaultValue: false}, {name: 'iSkipEndTime', defaultValue: 0}, {name: 'iSkipStartTime', defaultValue: 0}, {name: 'bFirstStart', defaultValue: true}, {name: 'sLastActiveTab', defaultValue: ''}, {name: 'bIsSettingsWindowOpen', defaultValue: false}, {name: 'oSettingsWindowPosition', defaultValue: {}} ]; for (const o of oVariables) { GM_setValue(o.name, GM_getValue(o.name) ?? o.defaultValue); } } /** Determines whether another hoster is available for the current episode. @returns {boolean} Whether another hoster is available. */ hasAnotherHoster() { return this.hasUrl([new RegExp(`https:\\/\\/(bs.to|burningseries.[a-z]{2,3})\\/.*[0-9]{1,3}\\/[0-9]{1,3}-.*\\/[a-z]+\\/(?!${this.getHoster(0)}|${this.getHoster(1)}|${this.getHoster(2)}|${this.getHoster(3)}).*`, 'g')]); } /** Determines whether the current page is a series page. @returns {boolean} Whether the page is a series page. */ isSeries() { return this.hasUrl([/^https:\/\/(bs.to|burningseries.[a-z]{2,3})\/serie\//g]); } /** Determines whether the current page is an episode page. @returns {boolean} Whether the page is an episode page. */ isEpisode() { return this.hasUrl([/^https:\/\/(bs.to|burningseries.[a-z]{2,3})/g, /[0-9]{1,3}\/[0-9]{1,3}-/g]); } /** Clicks the play button on the current episode page. @async @returns {Promise} A promise that resolves when the button has been clicked. */ async clickPlay() { return new Promise(async resolve => { let oPlayerElem = await this.waitForElement('section.serie .hoster-player') .catch(() => null); let iNumberOfClicks = 0; let iClickInterval = setInterval(async () => { if (oPlayerElem) { if ( document.querySelector('section.serie .hoster-player > a') || document.querySelector('section.serie .hoster-player > iframe') || iNumberOfClicks > 120 || this.querySelectorAllRegex('iframe', 'title', /recaptcha challenge/) ) { clearInterval(iClickInterval); resolve(); } iNumberOfClicks++; let oClickEvent = new Event('click'); oClickEvent.which = 1; oClickEvent.pageX = 6; oClickEvent.pageY = 1; oPlayerElem.dispatchEvent(oClickEvent); } }, 500); }); } /** Plays the next episode if the current video has ended. @param {boolean} [bSetEvent=true] - Whether to set the event listener to play the next episode. */ playNextEpisodeIfVideoEnded(bSetEvent = true) { if (!bSetEvent) { GM_removeValueChangeListener('isLocalVideoEnded'); return; } GM_addValueChangeListener('isLocalVideoEnded', () => { if (GM_getValue('isLocalVideoEnded')) { GM_setValue('isLocalVideoEnded', false); window.focus(); if (GM_getValue('bAutoplayRandomEpisode')) { let oRandomEpisode = document.querySelector('#sp_right > a'); document.location.replace(oRandomEpisode.href); } else { let oNextEpisode = document .querySelector('.serie .frame ul li[class^="e"].active ~ li:not(.disabled) a'); if (oNextEpisode) { document.location.replace(oNextEpisode.href); } else if (GM_getValue('bAutoplayNextSeason')) { let oNextSeason = document .querySelector('.serie .frame ul li[class^="s"].active ~ li:not(.disabled) a'); if (oNextSeason) { GM_setValue('clickFirstSeason', true); document.location.replace(oNextSeason.href); } } } } }); } /** Appends custom styles to the current page. */ appendOwnStyle() { const oStyle = document.createElement('style'); oStyle.id = 'xtrars-style'; // language=HTML oStyle.innerHTML = `