您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Set outro for any youtube channel and will automatically skip to next video when time is reached.
当前为
// ==UserScript== // @name Youtube Automatic BS Skip // @namespace http://tampermonkey.net/ // @version 2.7 // @description Set outro for any youtube channel and will automatically skip to next video when time is reached. // @author Daile Alimo // @license MIT // @match https://www.youtube.com/* // @grant GM.setValue // @grant GM.getValue // @grant GM.addStyle // @require http://code.jquery.com/jquery-latest.js // ==/UserScript== /* globals $ whenReady */ const app = "YouTube Automatic BS Skip"; const version = 2.7; const debug = false; // Elements const controlUI_ID = "outro-controls"; const modal_ID = "modal"; const progressBar_ID = "progress-bar"; const introTime_ID = "intro-set"; const outroTime_ID = "outro-set"; const introLen_ID = "intro-length"; const outroLen_ID = "outro-length"; const channelTxt_ID = "channel_txt"; // Actions const pauseOnOutro = "pause-on-outro"; const nextOnOutro = "next-on-outro"; const apply_ID = "apply"; // Misc const logoWidth = 94; const logoHeight = 50; const log = function(line) { if (debug) { console.log(line); } }; let initializationRequired = false; // whenReady - Keep checking the DOM until these given selectors are found. // Invokes a callback function on complete that contains an object containing the JQuery element(s) for the given selectors accessable with aliases if given. // // selectors[] - Each selector to await. // aliases[] - An alias for each selector/mutator. // mutators{} - Associative array/object that given alias as key and function as value and selector as arguments returns a calculated result in place of its selector. // callback - Function that is called when all selectors, containing each selector or its mutators returned value if applicable. // error - Function that is called when an error such as retries exceeded occurs. // maxRetries - The total number of times the whenReady will recur before calling the error function. const whenReady = function({selectors = [], aliases = [], mutators = {}, callback = (selectors = {}), error, maxRetries = 5}) { let ready = {}; let found = 0; for(let i in selectors){ let $sel = $(selectors[i]); if ($sel.length) { let index = aliases[i] ? aliases[i]: i; if (mutators[index]) { ready[index] = mutators[index]($sel); if (ready[index]){ found++; } } else { ready[index] = $sel; found++; } } } if (found === selectors.length) { return callback(ready); } setTimeout(function(){ if (maxRetries >= 1) { return whenReady({ selectors: selectors, aliases: aliases, mutators: mutators, callback: callback, maxRetries: --maxRetries }); } if (error !== undefined) { error("max retries exceeded"); } }, 500); }; // validateChannel - ensure we get a channel name out of the channel name element const validateChannel = function(selector) { let channel = selector.first().text(); log(`validating channel: ${channel}`); if (channel === "") { return false; } return channel; }; // // add indicators to the progress bar. const setupProgressBar = function(selector) { log('called setupProgressBar'); // add intro indicator to progress bar if (document.getElementById(`${progressBar_ID}-intro`) == null){ log('created intro indicator'); selector.prepend( $(`<div id="${progressBar_ID}-intro">`).addClass("ytp-load-progress").css({ "left": "0%", "transform": "scaleX(0)", }) ); } // add outro indicator to progress bar if (document.getElementById(`${progressBar_ID}-outro`) == null) { log('created outro indicator'); selector.prepend( $(`<div id="${progressBar_ID}-outro">`).addClass("ytp-load-progress").css({ "left": "100%", "transform": "scaleX(0)", }) ); } return [`${progressBar_ID}-intro`, `${progressBar_ID}-outro`]; }; // destroy the indicators added to the progressbar. const destroyProgressBar = function() { console.log("destroying progressbars"); if($(`#${progressBar_ID}-intro`).remove()){log("removed intro bar");} if($(`#${progressBar_ID}-outro`).remove()){log("removed outro bar");} }; // update the indecators on the progressbar. const updateProgressbars = function(intro, outro, duration) { // update the intro progress bar let introBar = $(`#${progressBar_ID}-intro`); var introFraction = intro / duration; introBar.css({ "left": "0%", "transform": `scaleX(${introFraction})`, "background-color": "green", }); // update the outro progress bar let outroBar = $(`#${progressBar_ID}-outro`); var outroFraction = outro / duration; outroBar.css({ "left": `${100 - (outroFraction * 100)}%`, "transform": `scaleX(${outroFraction})`, "background-color": "green", }); }; const setupControls = function(selector) { // Its easier to modify if we don't chain jquery.append($()) to build the html components var controls = document.getElementById(controlUI_ID); if (controls == null) { log('adding controls to video'); controls = selector.prepend(` <button id="${controlUI_ID}" class="ytp-button" data-tooltip-target-id="ytp-autonav-toggle-button" style="" title="YABSS" aria-label="YABSS"> <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> </button> `); } if (document.getElementById(modal_ID) == null) { log('adding modal to DOM'); $('body').append(` <div id="${modal_ID}"> <div id="modal-content"> <h3 id="${controlUI_ID}-title">${app} v${version} <a href="https://www.buymeacoffee.com/JustDai" target="_blank" style="position: relative; top: -5px; float: right"> <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"/></g> <g fill="#ffffff"><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"/></g></svg> </a> </h3> <div id="${controlUI_ID}-control-wrapper"> <input type="number" min="0" id="${introLen_ID}" placeholder="loading channel"/> <input type="number" min="0" id="${outroLen_ID}" placeholder="loading channel"/> <fieldset id="${controlUI_ID}-outro-action-group"> <div> <label for="${pauseOnOutro}">Pause</label> <input type="radio" name="outro-action-group" id="${pauseOnOutro}" /> </div> <div> <label for="${nextOnOutro}">Skip to next</label> <input type="radio" name="outro-action-group" id="${nextOnOutro}" checked /> <div> </fieldset> <div><span id="${channelTxt_ID}">loading</span> intro set: <span id="${introTime_ID}">0</span> seconds outro set: <span id="${outroTime_ID}">0</span> seconds</div> <tp-yt-paper-button id="${apply_ID}" class="style-scope ytd-video-secondary-info-renderer" style="text-align: center;">Apply</tp-yt-paper-button> </div> </div> </div>` ); } return controls; }; // Write the CSS rules to the DOM GM.addStyle(` #${controlUI_ID}-control-wrapper { display: block; clear: both; } #${controlUI_ID}-control-wrapper > * { display: inline-block; margin-bottom: 1em; } #${modal_ID} { display: none; min-width: 480px; max-width: 510px; box-shadow: 15px 19px 23px; border-radius: 8px; margin: auto; position: fixed; left: 18%; top: 20%; height: 14%; width: 60%; } #${modal_ID}.show { display: flex; } #modal-content { width: 100%; background-color: var(--yt-live-chat-moderator-color); border-radius: 6px 6px 6px; padding: 15px; color: white; } #${apply_ID} { position: relative; border: 1px solid white; } #${controlUI_ID} { height: 100%; padding: 0; margin: 0; bottom: 45%; position: relative; } #${controlUI_ID} svg { position: relative; top: 20%; left: 20%; } #${controlUI_ID}-panel { margin-right: 1em; vertical-align:top } #${controlUI_ID} > * { display: inline-block; max-height: 100%; } #${controlUI_ID}-title { padding: 2px; } #${controlUI_ID}-logo { display: inline-block; background-color: #c00; border-radius: 2px; cursor: pointer; text-align: center; color: white; } #${controlUI_ID}-logo {} #${controlUI_ID}-logo svg { max-height: 4em; opacity: .5; } #${controlUI_ID}-logo svg:hover { opacity: 1; } #${controlUI_ID}-outro-action-group { float: right; padding: .5em; } #${controlUI_ID}-outro-action-group > div { display: block; margin: auto; text-align-last: justify; } #${controlUI_ID}-logo #expand-actions { opacity: .5; text-align: center; position: relative; top: -.5em; } #${controlUI_ID}-logo #expand-actions:hover { opacity: 1; } #action-radios { display: none; } #action-radios .actions{ padding-left: 2px; text-align: left; background-color: black; color: white; } #${introLen_ID},#${outroLen_ID} { margin-right: 2px; } `); const destroyControls = function(){ log("destroying controls"); if($(`#${controlUI_ID}`).remove()){log("removed controls");} if($(`#${modal_ID}`).remove()){log("removed modal");} }; const updateControls = ({introPlaceholderTxt, outroPlaceholderTxt, channelTxt, introTxt, outroTxt, actions}) => { if (introPlaceholderTxt) { $(`#${introLen_ID}`).attr("placeholder", introPlaceholderTxt); } if (outroPlaceholderTxt) { $(`#${outroLen_ID}`).attr("placeholder", outroPlaceholderTxt); } if (introTxt) { $(`#${introTime_ID}`).text(introTxt); } if (outroTxt) { $(`#${outroTime_ID}`).text(outroTxt); } if (channelTxt) { $(`#${channelTxt_ID}`).text(channelTxt); } if (actions) { (actions.outro)? $(`#${nextOnOutro}`).attr("checked", "checked") : $(`#${pauseOnOutro}`).attr("checked", "checked"); } $(`#${modal_ID}`).removeClass("show"); }; const destroy = function(afterDetroyed){ log("destroying.."); destroyProgressBar(); destroyControls(); initializationRequired = false; log("destruction complete"); afterDetroyed(); }; (function(setupAndBind) { "use strict"; // // detect page change hashchange not working // so check every 3 seconds if current URL matches URL we started with. // handle appropriately. // var l = document.URL; if (l.includes("watch")) { log("on watch - calling setupAndBind"); try { setupAndBind(); } catch (e) {log(e.message)} } setInterval(function() { // check initializationRequired flag and if set, destroy and reinitialize. if (initializationRequired) { log("forced to destroy"); destroy(function() { log("rebuilding.."); setupAndBind(); }); } if (l != document.URL){ l = document.URL; if (l === "https://www.youtube.com/") { // ignore home destroy(function() { log("complete destruction"); }); } else if (l.includes("watch")) { log("channel changed"); initializationRequired = true } } }, 1500); })(function() { // ignore home if (document.URL === "https://www.youtube.com/"){ log("ignoring home"); return; } // whenReady({ // .ytp-progress-list selectors: [".video-stream", ".ytp-right-controls", ".ytp-progress-bar", "#meta-contents #text.ytd-channel-name,.ytp-ce-channel-title > a"], aliases: ["stream", "container", "progressBar", "channel"], mutators: { "container": setupControls, "progressBar": setupProgressBar, "channel": validateChannel, }, callback: async function(selectors) { // var channel = selectors.channel; var introTargetId = channel.split(" ").join("_") + "-intro"; var outroTargetId = channel.split(" ").join("_") + "-outro"; var outroAction = channel.split(" ").join("_") + "-outro-action"; log("loaded channel: " + channel); // var loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0); var loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0); var playNextOnOutro = await GM.getValue(outroAction, true); // log("intro set: " + loadedIntroSetInSeconds); log("outro set: " + loadedOutroSetInSeconds); log(`outro action: ${(playNextOnOutro)? "skip to next video": "pause"}`); // updateControls({ introPlaceholderTxt: (loadedIntroSetInSeconds <= 0)? "Set intro here..": loadedIntroSetInSeconds, outroPlaceholderTxt: (loadedOutroSetInSeconds <= 0)? "Set outro here..": loadedOutroSetInSeconds, channelTxt: channel, introTxt: loadedIntroSetInSeconds, outroTxt: loadedOutroSetInSeconds, actions: { outro: playNextOnOutro, }, }); // // const bindToStream = async function(){ // hook video timeupdate, wait for outro and hit next button when time reached // if update time less than intro, skip to intro time log("binding events"); let progressBarDone = false; let paused = false; let loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0); let loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0); log("intro set: " + loadedIntroSetInSeconds); log("outro set: " + loadedOutroSetInSeconds); // // set duration here and call writeProgressBars selectors.stream.unbind("timeupdate").on("timeupdate", function(e){ // use pause to prevent timeupdate after we have clicked pause button // there is a slight delay from when pause button is clicked, to when the timeupdates are stopped. if (paused) { return setTimeout(1000, () => {paused = false}); } // let currentTime = this.currentTime; let duration = this.duration; // if (duration && !progressBarDone) { progressBarDone = true; updateProgressbars(loadedIntroSetInSeconds, loadedOutroSetInSeconds, duration); } // If current time less than intro, skip past intro. if(currentTime < loadedIntroSetInSeconds) { this.currentTime = loadedIntroSetInSeconds; } // If current time greater or equal to outro, click next button or pause the stream. if(currentTime >= duration - loadedOutroSetInSeconds){ if (playNextOnOutro) { $(".ytp-next-button")[0].click(); } else { paused = true; $(".ytp-play-button")[0].click(); } } }); }; // // handle apply outro in seconds // log("bind to click"); // Control popup toggle button click listener $(`#${controlUI_ID}`).on('click', () => { console.log("toggle modal"); $(`#${modal_ID}`).toggleClass("show"); }); // Apply button click listener $(`#${apply_ID}`).on("click", function(e) { log("updating intro/outro skip"); var introSeconds = $("#" + introLen_ID).val().toString(); var outroSeconds = $("#" + outroLen_ID).val().toString(); if(introSeconds && introSeconds != "" && parseInt(introSeconds) != NaN){ if (introSeconds < 0) { introSeconds = 0; } // save outro in local storage GM.setValue(introTargetId, introSeconds); } if(outroSeconds && outroSeconds != "" && parseInt(outroSeconds) != NaN){ if (outroSeconds < 0) { outroSeconds = 0; } // save outro in local storage GM.setValue(outroTargetId, outroSeconds); } // update the intro/outro time on the controls updateControls({ introTxt: introSeconds, outroTxt: outroSeconds }); bindToStream(); }); // Pause on outro radio button change $(`#${pauseOnOutro}`).on("change", function(){ // pause on outro playNextOnOutro = false; GM.setValue(outroAction, playNextOnOutro); }); // Next on outro radio button change $(`#${nextOnOutro}`).on("change", function(){ // skip to next on outro playNextOnOutro = true; GM.setValue(outroAction, playNextOnOutro); }); // bindToStream(); }, error: function(e) { log(e); destroyControls(); destroyProgressBar(); }, }); });