Youtube Automatic BS Skip

A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels.

  1. // ==UserScript==
  2. // @name Youtube Automatic BS Skip
  3. // @namespace https://gf.qytechs.cn/en/scripts/392459-youtube-automatic-bs-skip
  4. // @source https://github.com/JustDaile/
  5. // @version 2.9.10
  6. // @description A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels.
  7. // @author Daile Alimo
  8. // @license MIT
  9. // @match https://www.youtube.com/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.addStyle
  14. // ==/UserScript==
  15. //
  16. /* globals $ whenReady trustedTypes */
  17.  
  18. const app = "YouTube Automatic BS Skip";
  19. const version = '2.9.10';
  20. const debug = false;
  21.  
  22. // Chrome: bypass content security policy, preventing innerHTML being set.
  23. // https://stackoverflow.com/questions/61964265/getting-error-this-document-requires-trustedhtml-assignment-in-chrome
  24.  
  25. // Detect if browser supports trustedTypes by checking if its defined.
  26. // 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.
  27.  
  28. let escapeHTMLPolicy;
  29. if (typeof trustedTypes !== "undefined") {
  30. escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {
  31. createHTML: (to_escape) => to_escape
  32. })
  33. } else {
  34. escapeHTMLPolicy = {
  35. createHTML: (to_escape) => to_escape
  36. };
  37. }
  38.  
  39. // Elements
  40. const yabssInputIdPrefix = "yabbs-control";
  41. const yabssModalIdPrefix = "yabbs-modal";
  42. const yabssProgressbarIdPrefix = "yabss-pgbar";
  43. const yabssIntroInputId = "yabbs-intro";
  44. const yabssOutroInputId = "yabbs-outro";
  45. const yabssChannelTxtContainerId = "yabbs-channel";
  46.  
  47. // Actions
  48. const pauseOnOutro = "pause-on-outro";
  49. const nextOnOutro = "next-on-outro";
  50. const instantNextOnFinish = "instant-next";
  51. const apply_ID = "apply";
  52.  
  53. // logs to console if debug is true
  54. const log = function() {
  55. if (debug) {
  56. console.log(...arguments);
  57. }
  58. };
  59.  
  60. log({
  61. app,
  62. version
  63. });
  64.  
  65. // updateControls updates only the elements in the modal controls, in which the values are set when function is invoked.
  66. const updateControls = ({introValue, outroValue, channelName, actions}) => {
  67. log('update controls');
  68. if (introValue !== undefined) {
  69. document.getElementById(yabssIntroInputId).value = introValue;
  70. }
  71. if (outroValue !== undefined) {
  72. document.getElementById(yabssOutroInputId).value = outroValue;
  73. }
  74. if (channelName !== undefined) {
  75. document.getElementById(yabssChannelTxtContainerId).innerText = channelName;
  76. }
  77. if (actions !== undefined) {
  78. actions.outro ? document.getElementById(nextOnOutro).checked = true : document.getElementById(pauseOnOutro).checked = true;
  79. actions.onFinish ? document.getElementById(instantNextOnFinish).checked = true : document.getElementById(instantNextOnFinish).checked = false;
  80. }
  81. document.getElementById(yabssModalIdPrefix).classList.remove('show');
  82. };
  83.  
  84. // asyncAwaitElements returns each of the selectors in a object with the provided aliases as keys to the found DOM element.
  85. const asyncAwaitElements = async (selectors, aliases, attempts = 5) => {
  86. return new Promise((resolve, reject) => {
  87. const id = setInterval(_ => {
  88. let ready = {};
  89. let found = 0;
  90. let count = 0;
  91. for(let i in selectors){
  92. let $sel = document.querySelector(selectors[i]);
  93. if ($sel) {
  94. let index = aliases[i] ? aliases[i]: i;
  95. log(`found selector ${selectors[i]}`);
  96. ready[index] = $sel;
  97. found++;
  98. }
  99. }
  100. if (found === selectors.length) {
  101. log("all selectors found");
  102. clearInterval(id);
  103. return resolve(ready);
  104. }
  105. if (count > attempts - 1) {
  106. reject(`reached max allowed attempts ${count}`);
  107. }
  108. count++
  109. }, 100)
  110. })
  111. }
  112.  
  113. (function(yabssApp) {
  114. "use strict";
  115. // dispose function provided by yabssApp
  116. var dispose;
  117. const ytapp = document.querySelector('body > ytd-app');
  118. // Quick channel loading - Hook into Youtube's events.
  119. // Best determined event for bootstrapping the applications lifecycle.
  120. // 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.
  121. ytapp.addEventListener("yt-page-data-fetched", async (e) => {
  122. const page = e.detail.pageData.page; // browse, watch
  123. log(page);
  124.  
  125. if (page !== 'watch') { // ignore any pages that are not 'watch'
  126. return
  127. }
  128. dispose = await yabssApp(e.detail.pageData.playerResponse.microformat.playerMicroformatRenderer.ownerChannelName);
  129. });
  130. // Dispose all event listeners whenever page navigation starts
  131. // When next video is loading YouTube resets video playback time to zero.
  132. // Since the binded timeupdate event is still running this causes last set intro to be skipped,
  133. // before the next video has loaded.
  134. // To get around this behaviour disposing all events listeners as soon as possible is best way to prevent this behaviour.
  135. ytapp.addEventListener("yt-navigate-start", (e) => {
  136. if (dispose) {
  137. dispose();
  138. dispose = null;
  139. }
  140. });
  141. })(async (channelName) => {
  142. log(`binding to ${channelName}`);
  143.  
  144. var paused = false;
  145. var continued = false;
  146.  
  147. const { stream, controlContainer, progressbar } = await asyncAwaitElements([".video-stream", ".ytp-right-controls", ".ytp-progress-bar"], ["stream", "controlContainer", "progressbar"])
  148. const controls = document.querySelector(yabssInputIdPrefix);
  149. if (controls == null) {
  150. log('adding modal toggle to video control panel.');
  151. controlContainer.insertBefore(videoControlButton, controlContainer.firstChild);
  152. }
  153.  
  154. // Pull channel settings
  155. var storeId = channelName.split(" ").join("_");
  156. var introTargetId = storeId + "-intro";
  157. var outroTargetId = storeId + "-outro";
  158. var outroActionId = storeId + "-outro-action";
  159. var finishedActionId = storeId + "-finished-action";
  160. var loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0);
  161. var loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0);
  162. var playNextOnOutro = await GM.getValue(outroActionId, true);
  163. var instantNextOnFinished = await GM.getValue(finishedActionId, true);
  164. log('channel settings', {
  165. channelName,
  166. loadedIntroSetInSeconds,
  167. loadedOutroSetInSeconds,
  168. playNextOnOutro,
  169. instantNextOnFinished
  170. });
  171.  
  172. // Setup & update progressbars
  173. var introBar = document.getElementById(`${yabssProgressbarIdPrefix}-intro`)
  174. if (introBar == null) {
  175. introBar = document.createElement('div')
  176. introBar.id = `${yabssProgressbarIdPrefix}-intro`
  177. introBar.classList.add('ytp-load-progress')
  178. introBar.style.left = "0%"
  179. introBar.style.transform = 'scaleX(0)'
  180. introBar.style.backgroundColor = "green"
  181. progressbar.insertBefore(introBar, progressbar.firstChild);
  182. }
  183.  
  184. var outroBar = document.getElementById(`${yabssProgressbarIdPrefix}-outro`)
  185. if (outroBar == null) {
  186. outroBar = document.createElement('div')
  187. outroBar.id = `${yabssProgressbarIdPrefix}-outro`
  188. outroBar.classList.add('ytp-load-progress')
  189. outroBar.style.left = '100%'
  190. outroBar.style.transform = 'scaleX(0)'
  191. outroBar.style.backgroundColor = "green"
  192. progressbar.insertBefore(outroBar, progressbar.firstChild);
  193. }
  194.  
  195. const updateProgressbars = (duration) => {
  196. var introFraction = loadedIntroSetInSeconds / duration;
  197. introBar.style.transform = `scaleX(${introFraction})`
  198.  
  199. var outroFraction = loadedOutroSetInSeconds / duration;
  200. outroBar.style.left = `${100 - (outroFraction * 100)}%`
  201. outroBar.style.transform = `scaleX(${outroFraction})`
  202. }
  203.  
  204. updateControls({ channelName, introValue: loadedIntroSetInSeconds, outroValue: loadedOutroSetInSeconds, actions: { outro: playNextOnOutro, onFinish: instantNextOnFinished } });
  205. const updateChannelSettings = _ => {
  206. loadedIntroSetInSeconds = document.getElementById(yabssIntroInputId).value;
  207. loadedOutroSetInSeconds = document.getElementById(yabssOutroInputId).value;
  208. GM.setValue(introTargetId, loadedIntroSetInSeconds);
  209. GM.setValue(outroTargetId, loadedOutroSetInSeconds);
  210. updateControls({
  211. introValue: loadedIntroSetInSeconds,
  212. outroValue: loadedOutroSetInSeconds
  213. });
  214. }
  215. document.getElementById(apply_ID).addEventListener('click', updateChannelSettings);
  216. const setPauseOnOutro = _ => {
  217. log('pause on outro changed');
  218. GM.setValue(outroActionId, false);
  219. playNextOnOutro=false
  220. }
  221. document.getElementById(pauseOnOutro).addEventListener('change', setPauseOnOutro);
  222. const setNextOnOutro = _ => {
  223. log('next on outro changed');
  224. GM.setValue(outroActionId, true);
  225. playNextOnOutro=true
  226. }
  227. document.getElementById(nextOnOutro).addEventListener('change', setNextOnOutro);
  228. const setInstantNextOnFinish = e => {
  229. log('instant next on finished changed');
  230. instantNextOnFinished=e.target.checked;
  231. GM.setValue(finishedActionId, instantNextOnFinished);
  232. }
  233. document.getElementById(instantNextOnFinish).addEventListener('change', setInstantNextOnFinish);
  234.  
  235. // Start watching timeupdates
  236. const onTimeUpdate = e => {
  237. const outroReached = e.target.currentTime >= e.target.duration - loadedOutroSetInSeconds
  238. updateProgressbars(e.target.duration);
  239.  
  240. // use pause to prevent timeupdate after script has clicked pause button.
  241. // There is a slight delay from when pause button is clicked, to when the timeupdates are stopped.
  242. // So this escape prevents further execution.
  243. if (paused) {
  244. return
  245. }
  246.  
  247. // If current time less than intro, skip past intro.
  248. if(e.target.currentTime < loadedIntroSetInSeconds) {
  249. log(`intro skipped ${loadedIntroSetInSeconds}`);
  250. e.target.currentTime = loadedIntroSetInSeconds;
  251. }
  252.  
  253. // If current time greater or equal to outro, click next button or pause the stream.
  254. if(outroReached){
  255. log('outro reached');
  256. if (playNextOnOutro) {
  257. log('auto-click next');
  258. document.querySelector('.ytp-next-button').click();
  259. } else if (!continued) {
  260. log('auto-click pause');
  261. document.querySelector('.ytp-play-button').click();
  262. paused=true;
  263. }
  264. }
  265. }
  266. stream.addEventListener('timeupdate', onTimeUpdate);
  267. const onPlay = e => {
  268. log(`onPlay`);
  269. // continued is when outro is reached and playback is resumed by the user.
  270. // However the user may skip back before pressing play.
  271. // So when resuming continued must first detect if it is still during the outro, if so playback will continue to the end of the video normally.
  272. continued = e.target.currentTime >= e.target.duration - loadedOutroSetInSeconds;
  273. // unpause timeupdates.
  274. paused = false;
  275. }
  276. stream.addEventListener('play', onPlay);
  277.  
  278. return _ => {
  279. log(`disposing event listeners`);
  280. stream.removeEventListener('timeupdate', onTimeUpdate);
  281. stream.removeEventListener('play', onPlay);
  282. document.getElementById(apply_ID).removeEventListener('click', updateChannelSettings);
  283. document.getElementById(pauseOnOutro).removeEventListener('change', setPauseOnOutro);
  284. document.getElementById(nextOnOutro).removeEventListener('change', setNextOnOutro);
  285. document.getElementById(instantNextOnFinish).removeEventListener('change', setInstantNextOnFinish);
  286. }
  287. })
  288.  
  289. // videoControlButton is the button that is displayed within the video controls.
  290. // click it will bring up the settings/controls modal.
  291. var videoControlButton = document.createElement('button')
  292. videoControlButton.innerHTML = escapeHTMLPolicy.createHTML(`
  293. <div class="ytp-autonav-toggle-button-container">
  294. <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>
  295. </div>`);
  296. videoControlButton.id = yabssInputIdPrefix;
  297. videoControlButton.classList.add('ytp-button');
  298. videoControlButton.setAttribute('title', app);
  299. videoControlButton.setAttribute('aria-label', app);
  300. log('created yabss button', videoControlButton);
  301.  
  302. // yabssPopupControls is the settings/controls modal that allows users to update settings for the channel.
  303. var yabssPopupControls = document.createElement('div');
  304. yabssPopupControls.id = yabssModalIdPrefix;
  305. yabssPopupControls.innerHTML = escapeHTMLPolicy.createHTML(`
  306. <div id="${yabssModalIdPrefix}-escape"></div>
  307. <div id="${yabssModalIdPrefix}-content">
  308. <div id="${yabssChannelTxtContainerId}">Loading Channel</div>
  309. <h2 id="${yabssInputIdPrefix}-title" class="d-flex justify-space-between">
  310. YouTube Automatic BS Skip ${version}
  311. <a href="https://www.buymeacoffee.com/JustDai" target="_blank">
  312. <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24">
  313. <g>
  314. <path d="M0,0h24v24H0V0z" fill="none"></path>
  315. </g>
  316. <g fill="var(--yt-live-chat-primary-text-color)">
  317. <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>
  318. </g>
  319. </svg>
  320. </a>
  321. </h2>
  322. <div id="${yabssInputIdPrefix}-control-wrapper">
  323. <div class="w-100 d-flex justify-space-around align-center">
  324. <label for="${yabssIntroInputId}">Intro</label>
  325. <input type="number" min="0" id="${yabssIntroInputId}" placeholder="unset" class="input">
  326. </div>
  327. <div class="w-100 d-flex justify-space-around align-center">
  328. <label for="${yabssOutroInputId}">Outro</label>
  329. <input type="number" min="0" id="${yabssOutroInputId}" placeholder="unset" class="input">
  330. </div>
  331. <div class="pa">
  332. <label for="${yabssInputIdPrefix}-outro-action-group">Action on outro:</label>
  333. <fieldset id="${yabssInputIdPrefix}-outro-action-group" class="d-flex">
  334. <div>
  335. <label for="${pauseOnOutro}">Pause Video</label>
  336. <input type="radio" name="outro-action-group" id="${pauseOnOutro}">
  337. </div>
  338. <div>
  339. <label for="${nextOnOutro}">Play Next Video</label>
  340. <input type="radio" name="outro-action-group" id="${nextOnOutro}" checked="checked">
  341. </div>
  342. </fieldset>
  343. </div>
  344. <div class="py" >
  345. <label for="${yabssInputIdPrefix}-ended-action-group">Action on finish:</label>
  346. <fieldset id="${yabssInputIdPrefix}-ended-action-group" class="d-flex">
  347. <div style="margin: auto; text-align: right;">
  348. <label for="${instantNextOnFinish}">Instantly play next</label>
  349. <input type="checkbox" name="outro-action-group" id="${instantNextOnFinish}">
  350. </div>
  351. </fieldset>
  352. </div>
  353. </div>
  354. <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>
  355. </div>`);
  356. document.body.insertAdjacentElement('beforeend', yabssPopupControls);
  357.  
  358. // toggleModalEventListener display or hide the yabssPopupControls.
  359. const toggleModalEventListener = _ => {
  360. log("toggling yabss modal");
  361. yabssPopupControls.classList.toggle("show");
  362. }
  363.  
  364. // Listen to user clicks on the video control button.
  365. videoControlButton.addEventListener('click', toggleModalEventListener);
  366.  
  367. // Listen to user clicks on modal escape area
  368. document.querySelector(`#${yabssModalIdPrefix}-escape`).addEventListener('click', toggleModalEventListener);
  369.  
  370. // Write the CSS rules to the DOM
  371. GM.addStyle(`
  372. #${yabssModalIdPrefix}-escape {
  373. position: fixed;
  374. left: 0;
  375. top: 0;
  376. width: 100vw;
  377. height: 100vh;
  378. z-index: 1000;
  379. }
  380. #${yabssModalIdPrefix} {
  381. display: none;
  382. position: fixed;
  383. left: 0;
  384. top: 0;
  385. width: 100vw;
  386. height: 100vh;
  387. z-index: 999;
  388. background: rgba(0,0,0,.8);
  389. }
  390. #${yabssModalIdPrefix}.show {
  391. display: flex;
  392. }
  393. #${yabssModalIdPrefix}-content {
  394. margin: auto;
  395. width: 30%;
  396. height: auto;
  397. background-color: var(--yt-live-chat-action-panel-background-color);
  398. color: var(--yt-live-chat-primary-text-color);
  399. border-radius: 6px 6px 6px;
  400. border: 1px solid var(--yt-live-chat-enabled-send-button-color);
  401. padding: 15px;
  402. z-index: 1001;
  403. box-shadow: 1em 1em 3em black;
  404. }
  405. #${yabssIntroInputId},#${yabssOutroInputId} {
  406. font-size: 1.2em;
  407. padding: .4em;
  408. border-radius: .5em;
  409. border: 1px solid var(--yt-live-chat-secondary-text-color);
  410. width: 80%;
  411. }
  412. #${apply_ID} {
  413. position: relative;
  414. border: 1px solid var(--yt-live-chat-secondary-text-color);
  415. transition: background-color .2s ease-in-out
  416. }
  417. #${apply_ID}:hover {
  418. background-color: var(--yt-spec-10-percent-layer);
  419. }
  420. #${yabssInputIdPrefix} {
  421. height: 100%;
  422. padding: 0;
  423. margin: 0;
  424. bottom: 45%;
  425. position: relative;
  426. }
  427. #${yabssInputIdPrefix} svg {
  428. position: relative;
  429. top: 20%;
  430. left: 20%;
  431. }
  432. #${yabssInputIdPrefix}-panel {
  433. margin-right: 1em;
  434. vertical-align:top
  435. }
  436. #${yabssInputIdPrefix} > * {
  437. display: inline-block;
  438. max-height: 100%;
  439. }
  440. #${yabssInputIdPrefix}-title {
  441. padding: 2px;
  442. }
  443. #${yabssInputIdPrefix}-outro-action-group {
  444. padding: .5em;
  445. }
  446. #${yabssInputIdPrefix}-outro-action-group > div {
  447. display: block;
  448. margin: auto;
  449. text-align-last: justify;
  450. }
  451. #${yabssInputIdPrefix}-control-wrapper > * {
  452. padding-top: 1em;
  453. }
  454. #action-radios {
  455. display: none;
  456. }
  457. #action-radios .actions {
  458. padding-left: 2px;
  459. text-align: left;
  460. background-color: var(--yt-spec-base-background);
  461. color: var(--yt-live-chat-secondary-text-color);
  462. }
  463. #${yabssIntroInputId},#${yabssOutroInputId} {
  464. margin-right: 2px;
  465. }
  466. #${yabssChannelTxtContainerId} {
  467. position: relative;
  468. top: -3.5em;
  469. margin-bottom: -1.5em;
  470. font-size: 1.1em;
  471. color: white;
  472. }
  473. .w-100 {
  474. width: 100% !important;
  475. }
  476. .input {
  477. padding: .2em;
  478. }
  479. .d-flex {
  480. display: flex;
  481. }
  482. .justify-center {
  483. justify-content: center;
  484. }
  485. .justify-space-around {
  486. justify-content: space-around;
  487. }
  488. .justify-space-between {
  489. justify-content: space-between;
  490. }
  491. .align-center {
  492. align-items: center;
  493. }
  494. .pa {
  495. padding: .5em;
  496. }
  497. .py {
  498. padding: .5em 0em;
  499. }
  500. `);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址