您需要先安装一个扩展,例如 篡改猴、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.4 // @description Set outro for any youtube channel and will automatically skip to next video when time is reached. // @author Daile Alimo // @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.4; const debug = false; const log = function(line) { if (debug) { console.log(line); } } // 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); }; // let initializationRequired = false; const destroy = function(afterDetroyed){ // // make sure we have no events binded, in the case fn() was called by interval on URL change // this will ensure that we can create clean controls for the current playlist without accidentally // having events persisting in the background. // log("destroying.."); if ($(".video-stream").unbind()) {log("unbinding .video-stream");} if ($("#set-outro").unbind()){log("unbinding #set-outro");} if ($("#outro-controls").remove()){log("removed controls");} if ($("#outro-bar").remove()){log("removed progressbar");} initializationRequired = false; afterDetroyed(); } // 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; } // const progressBar_ID = "progress-bar"; // add indicators to the progress bar. const setupProgressBar = function(selector) { destroyProgressBar(); // add intro indicator to progress bar selector.prepend( $(`<div id="${progressBar_ID}-intro">`).addClass("ytp-load-progress").css({ "left": "0%", "transform": "scaleX(0)", }) ); // add outro indicator to progress bar 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() { if($(`#${progressBar_ID}-intro`).remove()){log("removed intro bar");} if($(`#${progressBar_ID}-outro`).remove()){log("removed outro bar");} } // create the indecators on the progressbar. const createProgressBars = 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 controlUI_ID = "outro-controls"; 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"; // end actions const apply_ID = "apply"; const logoWidth = 94; const logoHeight = 50; const setupControls = function(selector) { destroyControls(); // Its easier to modify if we don't chain jquery.append($()) to build the html components // let controls = selector.prepend(` <div id="${controlUI_ID}"> <div id="${controlUI_ID}-panel" style="display: none;"> <h3 id="${controlUI_ID}-title">${app} v${version} - <a href="https://www.buymeacoffee.com/JustDai" target="_blank">Support me <3</a></h3> <input type="number" min="0" id="${introLen_ID}" placeholder="loading channel"/> <input type="number" min="0" id="${outroLen_ID}" placeholder="loading channel"/> <button id="${apply_ID}">apply</button> <div><span id="channel_txt">loading</span> intro set: <span id="${introTime_ID}">0</span> seconds outro set: <span id="${outroTime_ID}">0</span> seconds</div> </div> <div id="${controlUI_ID}-logo"> <svg width="${logoWidth}" height="${logoHeight}" viewBox="-34 -10 452.5 112.5"><g id="SvgjsG1230" featurekey="symbolFeature-0" transform="matrix(0.2018017853791305,0,0,0.2018017853791305,-13.823422298470438,0.17657770152956154)" fill="#ffffff"><g xmlns="http://www.w3.org/2000/svg"><path d="M136.3,349.2L287,198.4c-7.1-2.6-12.3-9.4-12.3-17.4c0-10.4,8.4-18.8,18.8-18.8c8.1,0,14.8,5.1,17.4,12.3l20.4-20.4 c-3.3-2.8-6.5-5.6-10-8.1l26.1-63.2l-34.7-14.3l-25.2,61.1c-10.1-3-20.7-4.8-31.6-4.8c-10.9,0-21.5,1.8-31.6,4.8l-25.2-61.1 l-34.7,14.3l26.1,63.2c-18.7,13.1-34.3,31.6-45.7,53.7H87.3v37.5h43.5c-3,12-4.8,24.5-5.5,37.5H68.5v37.5h58.4 C128.8,325.2,132,337.5,136.3,349.2z M218.5,162.2c10.4,0,18.8,8.4,18.8,18.8c0,10.4-8.4,18.8-18.8,18.8s-18.8-8.4-18.8-18.8 C199.8,170.6,208.1,162.2,218.5,162.2z"></path><path d="M443.5,95.1L95.1,443.5h53l30.7-30.7c21.7,19.2,48.3,30.7,77.1,30.7c40.1,0,75.9-21.9,100-56.3h68.8v-37.5h-49.2 c4.4-11.8,7.6-24.4,9.6-37.5h58.4v-37.5h-56.8c-0.6-13-2.4-25.5-5.5-37.5h43.5v-37.5h-32.8l51.6-51.6V95.1z"></path></g></g><g id="SvgjsG1231" featurekey="nameFeature-0" transform="matrix(3.452357175253672,0,0,3.452357175253672,96.00000164621218,-37.28546802849764)" fill="#ffffff"><path d="M10.8 11.399999999999999 l1.28 0 l-5.4 10.84 l0 17.76 l-1.28 0 l0 -17.76 l-5.4 -10.84 l1.28 0 l4.76 9.52 z M30.515 40 l-1.12 -4.52 l-10.96 0 l-1.12 4.52 l-1.2 0 l7.08 -28.6 l1.44 0 l7.08 28.6 l-1.2 0 z M18.715 34.28 l10.4 0 l-5.2 -20.88 z M39.63 20.88 c4.08 1.16 7.04 4.92 7.04 9.36 c0 5.4 -4.36 9.76 -9.76 9.76 l-1.16 0 l0 -28.6 l1.16 0 c2.84 0 5.12 2.28 5.12 5.12 c0 1.84 -0.96 3.44 -2.4 4.36 z M36.91 12.559999999999999 l0 7.92 c2.2 0 3.96 -1.76 3.96 -3.96 s-1.76 -3.96 -3.96 -3.96 z M36.91 38.84 c4.76 0 8.6 -3.88 8.6 -8.6 c0 -4.76 -3.84 -8.6 -8.6 -8.6 l0 17.2 l0 0 z M54.705 40.56 c-2.36 0 -4.6 -1.12 -6 -3.04 l0.92 -0.68 c1.2 1.6 3.08 2.56 5.08 2.56 c3.4 0 6.2 -2.8 6.2 -6.24 c0 -3.56 -2.72 -6.16 -5.36 -8.72 c-2.56 -2.4 -5.16 -4.92 -5.16 -8.24 c0 -2.96 2.44 -5.4 5.4 -5.4 c1.72 0 3.36 0.84 4.4 2.24 l-0.96 0.68 c-0.8 -1.08 -2.08 -1.76 -3.44 -1.76 c-2.32 0 -4.24 1.92 -4.24 4.24 c0 2.8 2.32 5.04 4.8 7.4 c2.8 2.72 5.72 5.52 5.72 9.56 c0 4.08 -3.32 7.4 -7.36 7.4 z M72.1 40.56 c-2.36 0 -4.6 -1.12 -6 -3.04 l0.92 -0.68 c1.2 1.6 3.08 2.56 5.08 2.56 c3.4 0 6.2 -2.8 6.2 -6.24 c0 -3.56 -2.72 -6.16 -5.36 -8.72 c-2.56 -2.4 -5.16 -4.92 -5.16 -8.24 c0 -2.96 2.44 -5.4 5.4 -5.4 c1.72 0 3.36 0.84 4.4 2.24 l-0.96 0.68 c-0.8 -1.08 -2.08 -1.76 -3.44 -1.76 c-2.32 0 -4.24 1.92 -4.24 4.24 c0 2.8 2.32 5.04 4.8 7.4 c2.8 2.72 5.72 5.52 5.72 9.56 c0 4.08 -3.32 7.4 -7.36 7.4 z"></path></g></svg> <div id="expand-actions"><span>More</span></div> <div id="action-radios"> <h4 class="actions">On outro:</h4> <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> </div> </div> `); $(`#${controlUI_ID}-logo svg`).on("click", function(){ $(`#${controlUI_ID}-panel`).fadeToggle("slow"); }); $("#expand-actions").on("click", function(){ $("#action-radios").slideToggle("fast", "swing"); }); return controls; } // Write the CSS rules to the DOM GM.addStyle(` #${controlUI_ID} { display: block; margin: 5px; text-align: right; color: #666666; } #${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: both; padding: .5em; background-color: #800; } #${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(){ if($(`#${controlUI_ID}`).remove()){log("removed controls");} } const updateControls = ({introPlaceholderTxt, outroPlaceholderTxt, channelTxt, introTxt, outroTxt, actions}) => { if (introPlaceholderTxt) { $(`#${introLen_ID}`).attr("placeholder", introPlaceholderTxt); } if (outroPlaceholderTxt) { $(`#${outroLen_ID}`).attr("placeholder", outroPlaceholderTxt); } if (channelTxt) { $(`#${channelTxt_ID}`).text(channelTxt); } if (introTxt) { $(`#${introTime_ID}`).text(introTxt); } if (outroTxt) { $(`#${outroTime_ID}`).text(outroTxt); } if (actions) { (actions.outro)? $(`#${nextOnOutro}`).attr("checked", "checked") : $(`#${pauseOnOutro}`).attr("checked", "checked"); } } (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")) { setupAndBind(); } 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 } } }, 3000); })(function() { // ignore home if (document.URL === "https://www.youtube.com/"){ log("ignoring home"); return; } // whenReady({ // .ytp-progress-list selectors: [".video-stream", "#primary > #primary-inner", ".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; createProgressBars(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"); $(`#${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(); }); // options $(`#${pauseOnOutro}`).on("change", function(){ // pause on outro playNextOnOutro = false; GM.setValue(outroAction, playNextOnOutro); }); $(`#${nextOnOutro}`).on("change", function(){ // skip to next on outro playNextOnOutro = true; GM.setValue(outroAction, playNextOnOutro); }); // bindToStream(); }, error: function(e) { log(e); destroyControls(); destroyProgressBar(); }, }); });