A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels.
Вам также может понравится Remove Facebook Video Advertisements & Sponsored Content.
// ==UserScript== // @name Youtube Automatic BS Skip // @namespace https://greasyfork.org/en/scripts/392459-youtube-automatic-bs-skip // @source https://github.com/JustDaile/ // @version 2.9.10 // @description A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels. // @author Daile Alimo // @license MIT // @match https://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant GM.setValue // @grant GM.getValue // @grant GM.addStyle // ==/UserScript== // /* globals $ whenReady trustedTypes */ const app = "YouTube Automatic BS Skip"; const version = '2.9.10'; const debug = false; // Chrome: bypass content security policy, preventing innerHTML being set. // https://stackoverflow.com/questions/61964265/getting-error-this-document-requires-trustedhtml-assignment-in-chrome // Detect if browser supports trustedTypes by checking if its defined. // If not defined mock the createHTML method to negate the need to make additional updates to the codebase depending on if trustedTypes is used or not. let escapeHTMLPolicy; if (typeof trustedTypes !== "undefined") { escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", { createHTML: (to_escape) => to_escape }) } else { escapeHTMLPolicy = { createHTML: (to_escape) => to_escape }; } // Elements const yabssInputIdPrefix = "yabbs-control"; const yabssModalIdPrefix = "yabbs-modal"; const yabssProgressbarIdPrefix = "yabss-pgbar"; const yabssIntroInputId = "yabbs-intro"; const yabssOutroInputId = "yabbs-outro"; const yabssChannelTxtContainerId = "yabbs-channel"; // Actions const pauseOnOutro = "pause-on-outro"; const nextOnOutro = "next-on-outro"; const instantNextOnFinish = "instant-next"; const apply_ID = "apply"; // logs to console if debug is true const log = function() { if (debug) { console.log(...arguments); } }; log({ app, version }); // updateControls updates only the elements in the modal controls, in which the values are set when function is invoked. const updateControls = ({introValue, outroValue, channelName, actions}) => { log('update controls'); if (introValue !== undefined) { document.getElementById(yabssIntroInputId).value = introValue; } if (outroValue !== undefined) { document.getElementById(yabssOutroInputId).value = outroValue; } if (channelName !== undefined) { document.getElementById(yabssChannelTxtContainerId).innerText = channelName; } if (actions !== undefined) { actions.outro ? document.getElementById(nextOnOutro).checked = true : document.getElementById(pauseOnOutro).checked = true; actions.onFinish ? document.getElementById(instantNextOnFinish).checked = true : document.getElementById(instantNextOnFinish).checked = false; } document.getElementById(yabssModalIdPrefix).classList.remove('show'); }; // asyncAwaitElements returns each of the selectors in a object with the provided aliases as keys to the found DOM element. const asyncAwaitElements = async (selectors, aliases, attempts = 5) => { return new Promise((resolve, reject) => { const id = setInterval(_ => { let ready = {}; let found = 0; let count = 0; for(let i in selectors){ let $sel = document.querySelector(selectors[i]); if ($sel) { let index = aliases[i] ? aliases[i]: i; log(`found selector ${selectors[i]}`); ready[index] = $sel; found++; } } if (found === selectors.length) { log("all selectors found"); clearInterval(id); return resolve(ready); } if (count > attempts - 1) { reject(`reached max allowed attempts ${count}`); } count++ }, 100) }) } (function(yabssApp) { "use strict"; // dispose function provided by yabssApp var dispose; const ytapp = document.querySelector('body > ytd-app'); // Quick channel loading - Hook into Youtube's events. // Best determined event for bootstrapping the applications lifecycle. // YouTube calls yt-page-data-fetched when page when page/channel information has been loaded, but way sooner than it takes to update the UI. ytapp.addEventListener("yt-page-data-fetched", async (e) => { const page = e.detail.pageData.page; // browse, watch log(page); if (page !== 'watch') { // ignore any pages that are not 'watch' return } dispose = await yabssApp(e.detail.pageData.playerResponse.microformat.playerMicroformatRenderer.ownerChannelName); }); // Dispose all event listeners whenever page navigation starts // When next video is loading YouTube resets video playback time to zero. // Since the binded timeupdate event is still running this causes last set intro to be skipped, // before the next video has loaded. // To get around this behaviour disposing all events listeners as soon as possible is best way to prevent this behaviour. ytapp.addEventListener("yt-navigate-start", (e) => { if (dispose) { dispose(); dispose = null; } }); })(async (channelName) => { log(`binding to ${channelName}`); var paused = false; var continued = false; const { stream, controlContainer, progressbar } = await asyncAwaitElements([".video-stream", ".ytp-right-controls", ".ytp-progress-bar"], ["stream", "controlContainer", "progressbar"]) const controls = document.querySelector(yabssInputIdPrefix); if (controls == null) { log('adding modal toggle to video control panel.'); controlContainer.insertBefore(videoControlButton, controlContainer.firstChild); } // Pull channel settings var storeId = channelName.split(" ").join("_"); var introTargetId = storeId + "-intro"; var outroTargetId = storeId + "-outro"; var outroActionId = storeId + "-outro-action"; var finishedActionId = storeId + "-finished-action"; var loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0); var loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0); var playNextOnOutro = await GM.getValue(outroActionId, true); var instantNextOnFinished = await GM.getValue(finishedActionId, true); log('channel settings', { channelName, loadedIntroSetInSeconds, loadedOutroSetInSeconds, playNextOnOutro, instantNextOnFinished }); // Setup & update progressbars var introBar = document.getElementById(`${yabssProgressbarIdPrefix}-intro`) if (introBar == null) { introBar = document.createElement('div') introBar.id = `${yabssProgressbarIdPrefix}-intro` introBar.classList.add('ytp-load-progress') introBar.style.left = "0%" introBar.style.transform = 'scaleX(0)' introBar.style.backgroundColor = "green" progressbar.insertBefore(introBar, progressbar.firstChild); } var outroBar = document.getElementById(`${yabssProgressbarIdPrefix}-outro`) if (outroBar == null) { outroBar = document.createElement('div') outroBar.id = `${yabssProgressbarIdPrefix}-outro` outroBar.classList.add('ytp-load-progress') outroBar.style.left = '100%' outroBar.style.transform = 'scaleX(0)' outroBar.style.backgroundColor = "green" progressbar.insertBefore(outroBar, progressbar.firstChild); } const updateProgressbars = (duration) => { var introFraction = loadedIntroSetInSeconds / duration; introBar.style.transform = `scaleX(${introFraction})` var outroFraction = loadedOutroSetInSeconds / duration; outroBar.style.left = `${100 - (outroFraction * 100)}%` outroBar.style.transform = `scaleX(${outroFraction})` } updateControls({ channelName, introValue: loadedIntroSetInSeconds, outroValue: loadedOutroSetInSeconds, actions: { outro: playNextOnOutro, onFinish: instantNextOnFinished } }); const updateChannelSettings = _ => { loadedIntroSetInSeconds = document.getElementById(yabssIntroInputId).value; loadedOutroSetInSeconds = document.getElementById(yabssOutroInputId).value; GM.setValue(introTargetId, loadedIntroSetInSeconds); GM.setValue(outroTargetId, loadedOutroSetInSeconds); updateControls({ introValue: loadedIntroSetInSeconds, outroValue: loadedOutroSetInSeconds }); } document.getElementById(apply_ID).addEventListener('click', updateChannelSettings); const setPauseOnOutro = _ => { log('pause on outro changed'); GM.setValue(outroActionId, false); playNextOnOutro=false } document.getElementById(pauseOnOutro).addEventListener('change', setPauseOnOutro); const setNextOnOutro = _ => { log('next on outro changed'); GM.setValue(outroActionId, true); playNextOnOutro=true } document.getElementById(nextOnOutro).addEventListener('change', setNextOnOutro); const setInstantNextOnFinish = e => { log('instant next on finished changed'); instantNextOnFinished=e.target.checked; GM.setValue(finishedActionId, instantNextOnFinished); } document.getElementById(instantNextOnFinish).addEventListener('change', setInstantNextOnFinish); // Start watching timeupdates const onTimeUpdate = e => { const outroReached = e.target.currentTime >= e.target.duration - loadedOutroSetInSeconds updateProgressbars(e.target.duration); // use pause to prevent timeupdate after script has clicked pause button. // There is a slight delay from when pause button is clicked, to when the timeupdates are stopped. // So this escape prevents further execution. if (paused) { return } // If current time less than intro, skip past intro. if(e.target.currentTime < loadedIntroSetInSeconds) { log(`intro skipped ${loadedIntroSetInSeconds}`); e.target.currentTime = loadedIntroSetInSeconds; } // If current time greater or equal to outro, click next button or pause the stream. if(outroReached){ log('outro reached'); if (playNextOnOutro) { log('auto-click next'); document.querySelector('.ytp-next-button').click(); } else if (!continued) { log('auto-click pause'); document.querySelector('.ytp-play-button').click(); paused=true; } } } stream.addEventListener('timeupdate', onTimeUpdate); const onPlay = e => { log(`onPlay`); // continued is when outro is reached and playback is r###med by the user. // However the user may skip back before pressing play. // So when r###ming continued must first detect if it is still during the outro, if so playback will continue to the end of the video normally. continued = e.target.currentTime >= e.target.duration - loadedOutroSetInSeconds; // unpause timeupdates. paused = false; } stream.addEventListener('play', onPlay); return _ => { log(`disposing event listeners`); stream.removeEventListener('timeupdate', onTimeUpdate); stream.removeEventListener('play', onPlay); document.getElementById(apply_ID).removeEventListener('click', updateChannelSettings); document.getElementById(pauseOnOutro).removeEventListener('change', setPauseOnOutro); document.getElementById(nextOnOutro).removeEventListener('change', setNextOnOutro); document.getElementById(instantNextOnFinish).removeEventListener('change', setInstantNextOnFinish); } }) // videoControlButton is the button that is displayed within the video controls. // click it will bring up the settings/controls modal. var videoControlButton = document.createElement('button') videoControlButton.innerHTML = escapeHTMLPolicy.createHTML(` <div class="ytp-autonav-toggle-button-container"> <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path fill="white" d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/></svg> </div>`); videoControlButton.id = yabssInputIdPrefix; videoControlButton.classList.add('ytp-button'); videoControlButton.setAttribute('title', app); videoControlButton.setAttribute('aria-label', app); log('created yabss button', videoControlButton); // yabssPopupControls is the settings/controls modal that allows users to update settings for the channel. var yabssPopupControls = document.createElement('div'); yabssPopupControls.id = yabssModalIdPrefix; yabssPopupControls.innerHTML = escapeHTMLPolicy.createHTML(` <div id="${yabssModalIdPrefix}-escape"></div> <div id="${yabssModalIdPrefix}-content"> <div id="${yabssChannelTxtContainerId}">Loading Channel</div> <h2 id="${yabssInputIdPrefix}-title" class="d-flex justify-space-between"> YouTube Automatic BS Skip ${version} <a href="https://www.buymeacoffee.com/JustDai" target="_blank"> <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"> <g> <path d="M0,0h24v24H0V0z" fill="none"></path> </g> <g fill="var(--yt-live-chat-primary-text-color)"> <path d="M18.5,3H6C4.9,3,4,3.9,4,5v5.71c0,3.83,2.95,7.18,6.78,7.29c3.96,0.12,7.22-3.06,7.22-7v-1h0.5c1.93,0,3.5-1.57,3.5-3.5 S20.43,3,18.5,3z M16,5v3H6V5H16z M18.5,8H18V5h0.5C19.33,5,20,5.67,20,6.5S19.33,8,18.5,8z M4,19h16v2H4V19z"></path> </g> </svg> </a> </h2> <div id="${yabssInputIdPrefix}-control-wrapper"> <div class="w-100 d-flex justify-space-around align-center"> <label for="${yabssIntroInputId}">Intro</label> <input type="number" min="0" id="${yabssIntroInputId}" placeholder="unset" class="input"> </div> <div class="w-100 d-flex justify-space-around align-center"> <label for="${yabssOutroInputId}">Outro</label> <input type="number" min="0" id="${yabssOutroInputId}" placeholder="unset" class="input"> </div> <div class="pa"> <label for="${yabssInputIdPrefix}-outro-action-group">Action on outro:</label> <fieldset id="${yabssInputIdPrefix}-outro-action-group" class="d-flex"> <div> <label for="${pauseOnOutro}">Pause Video</label> <input type="radio" name="outro-action-group" id="${pauseOnOutro}"> </div> <div> <label for="${nextOnOutro}">Play Next Video</label> <input type="radio" name="outro-action-group" id="${nextOnOutro}" checked="checked"> </div> </fieldset> </div> <div class="py" > <label for="${yabssInputIdPrefix}-ended-action-group">Action on finish:</label> <fieldset id="${yabssInputIdPrefix}-ended-action-group" class="d-flex"> <div style="margin: auto; text-align: right;"> <label for="${instantNextOnFinish}">Instantly play next</label> <input type="checkbox" name="outro-action-group" id="${instantNextOnFinish}"> </div> </fieldset> </div> </div> <tp-yt-paper-button id="${apply_ID}" class="style-scope py ytd-video-secondary-info-renderer d-flex justify-center align-center" style-target="host" role="button" elevation="3" aria-disabled="false">${apply_ID}</tp-yt-paper-button> </div>`); document.body.insertAdjacentElement('beforeend', yabssPopupControls); // toggleModalEventListener display or hide the yabssPopupControls. const toggleModalEventListener = _ => { log("toggling yabss modal"); yabssPopupControls.classList.toggle("show"); } // Listen to user clicks on the video control button. videoControlButton.addEventListener('click', toggleModalEventListener); // Listen to user clicks on modal escape area document.querySelector(`#${yabssModalIdPrefix}-escape`).addEventListener('click', toggleModalEventListener); // Write the CSS rules to the DOM GM.addStyle(` #${yabssModalIdPrefix}-escape { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 1000; } #${yabssModalIdPrefix} { display: none; position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 999; background: rgba(0,0,0,.8); } #${yabssModalIdPrefix}.show { display: flex; } #${yabssModalIdPrefix}-content { margin: auto; width: 30%; height: auto; background-color: var(--yt-live-chat-action-panel-background-color); color: var(--yt-live-chat-primary-text-color); border-radius: 6px 6px 6px; border: 1px solid var(--yt-live-chat-enabled-send-button-color); padding: 15px; z-index: 1001; box-shadow: 1em 1em 3em black; } #${yabssIntroInputId},#${yabssOutroInputId} { font-size: 1.2em; padding: .4em; border-radius: .5em; border: 1px solid var(--yt-live-chat-secondary-text-color); width: 80%; } #${apply_ID} { position: relative; border: 1px solid var(--yt-live-chat-secondary-text-color); transition: background-color .2s ease-in-out } #${apply_ID}:hover { background-color: var(--yt-spec-10-percent-layer); } #${yabssInputIdPrefix} { height: 100%; padding: 0; margin: 0; bottom: 45%; position: relative; } #${yabssInputIdPrefix} svg { position: relative; top: 20%; left: 20%; } #${yabssInputIdPrefix}-panel { margin-right: 1em; vertical-align:top } #${yabssInputIdPrefix} > * { display: inline-block; max-height: 100%; } #${yabssInputIdPrefix}-title { padding: 2px; } #${yabssInputIdPrefix}-outro-action-group { padding: .5em; } #${yabssInputIdPrefix}-outro-action-group > div { display: block; margin: auto; text-align-last: justify; } #${yabssInputIdPrefix}-control-wrapper > * { padding-top: 1em; } #action-radios { display: none; } #action-radios .actions { padding-left: 2px; text-align: left; background-color: var(--yt-spec-base-background); color: var(--yt-live-chat-secondary-text-color); } #${yabssIntroInputId},#${yabssOutroInputId} { margin-right: 2px; } #${yabssChannelTxtContainerId} { position: relative; top: -3.5em; margin-bottom: -1.5em; font-size: 1.1em; color: white; } .w-100 { width: 100% !important; } .input { padding: .2em; } .d-flex { display: flex; } .justify-center { justify-content: center; } .justify-space-around { justify-content: space-around; } .justify-space-between { justify-content: space-between; } .align-center { align-items: center; } .pa { padding: .5em; } .py { padding: .5em 0em; } `);