Simple Sponsor Skipper

Skips annoying intros, sponsors and w/e on YouTube and its frontends like Invidious and CloudTube using the SponsorBlock API.

  1. // ==UserScript==
  2. // @name Simple Sponsor Skipper
  3. // @author mthsk
  4. // @homepage https://codeberg.org/mthsk/userscripts/src/branch/master/simple-sponsor-skipper
  5. // @match *://m.youtube.com/*
  6. // @match *://youtu.be/*
  7. // @match *://www.youtube.com/*
  8. // @match *://www.youtube-nocookie.com/embed/*
  9. // @match *://odysee.com/*
  10. // @match *://yt.artemislena.eu/*
  11. // @match *://tube.cadence.moe/*
  12. // @match *://y.com.sb/*
  13. // @match *://invidious.esmailelbob.xyz/*
  14. // @match *://invidious.flokinet.to/*
  15. // @match *://inv.frail.com.br/*
  16. // @match *://invidious.garudalinux.org/*
  17. // @match *://invidious.kavin.rocks/*
  18. // @match *://inv.nadeko.net/*
  19. // @match *://invidious.namazso.eu/*
  20. // @match *://iv.nboeck.de/*
  21. // @match *://invidious.nerdvpn.de/*
  22. // @match *://youtube.owacon.moe/*
  23. // @match *://inv.pistasjis.net/*
  24. // @match *://invidious.projectsegfau.lt/*
  25. // @match *://inv.bp.projectsegfau.lt/*
  26. // @match *://inv.in.projectsegfau.lt/*
  27. // @match *://inv.us.projectsegfau.lt/*
  28. // @match *://vid.puffyan.us/*
  29. // @match *://invidious.sethforprivacy.com/*
  30. // @match *://invidious.slipfox.xyz/*
  31. // @match *://invidious.snopyta.org/*
  32. // @match *://inv.vern.cc/*
  33. // @match *://invidious.weblibre.org/*
  34. // @match *://youchu.be/*
  35. // @match *://yewtu.be/*
  36. // @grant GM.getValue
  37. // @grant GM.setValue
  38. // @grant GM.notification
  39. // @grant GM.openInTab
  40. // @grant GM.registerMenuCommand
  41. // @grant GM.xmlHttpRequest
  42. // @allFrames true
  43. // @connect sponsor.ajay.app
  44. // @connect *
  45. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  46. // @run-at document-start
  47. // @version 2024.06
  48. // @license AGPL-3.0-or-later
  49. // @description Skips annoying intros, sponsors and w/e on YouTube and its frontends like Invidious and CloudTube using the SponsorBlock API.
  50. // @namespace https://greasyfork.org/users/751327
  51. // ==/UserScript==
  52. /**
  53. * This program is free software: you can redistribute it and/or modify
  54. * it under the terms of the GNU Affero General Public License as
  55. * published by the Free Software Foundation, either version 3 of the
  56. * License, or (at your option) any later version.
  57. *
  58. * This program is distributed in the hope that it will be useful,
  59. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  60. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  61. * GNU Affero General Public License for more details.
  62. *
  63. * You should have received a copy of the GNU Affero General Public License
  64. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  65. */
  66. (async function() {
  67. "use strict";
  68.  
  69. if (typeof GM.registerMenuCommand == 'undefined') //safari
  70. this.GM.registerMenuCommand = () => console.log((new Date()).toTimeString().split(' ')[0] + " - Simple Sponsor Skipper: Menu comments are not currently supported by your Script Manager.");
  71.  
  72. if (typeof GM.notification == 'undefined') //safari
  73. this.GM.notification = () => console.log((new Date()).toTimeString().split(' ')[0] + " - Simple Sponsor Skipper: Notifications are not currently supported by your Script Manager.");
  74.  
  75. async function go(videoId) {
  76. console.log("New video ID: " + videoId);
  77.  
  78. const inst = s3settings.instance || "sponsor.ajay.app";
  79. let segurl = "";
  80. let result = [];
  81. let rBefore = -1;
  82. let rPoi = null;
  83. const cat = encodeURIComponent(JSON.stringify(shuffle(s3settings.categories)));
  84.  
  85. if (s3settings.disable_hashing)
  86. {
  87. segurl = 'https://' + inst + '/api/skipSegments?videoID=' + videoId + "&categories=" + cat;
  88. }
  89. else
  90. {
  91. const vidsha256 = await sha256(videoId);
  92. console.log("SHA256 hash: " + vidsha256);
  93. segurl = 'https://' + inst + '/api/skipSegments/' + vidsha256.substring(0,4) + "?categories=" + cat;
  94. }
  95. console.log(segurl);
  96.  
  97. const resp = await (() => {
  98. return new Promise(resolve => {
  99. GM.xmlHttpRequest({
  100. method: 'GET',
  101. url: segurl,
  102. headers: {
  103. 'Accept': 'application/json'
  104. },
  105. onload: resolve
  106. });
  107. });
  108. })();
  109. try {
  110. const response = s3settings.disable_hashing ? JSON.parse("[{\"videoID\":\"" + videoId + "\",\"segments\":" + resp.responseText + "}]") : JSON.parse(resp.responseText);
  111.  
  112. for (let x = 0; x < response.length; x++)
  113. {
  114. if (response[x].videoID === videoId)
  115. {
  116. rBefore = response[x].segments.length;
  117. result = processSegments(response[x].segments);
  118. if (result[result.length - 1].category === "poi_highlight")
  119. {
  120. rPoi = result[result.length - 1].segment[0];
  121. result.splice((result.length - 1), 1);
  122. }
  123. break;
  124. }
  125. }
  126. } catch (e) { result = []; }
  127. let x = 0;
  128. let prevTime = -1;
  129. const favicon = !!document.head.querySelector('link[rel=icon][href]') ? document.head.querySelector('link[rel=icon][href]').href : undefined; // document.head.querySelector('link[rel=icon][href]')?.href; <-- Syntax error on Pale Moon with Greasemonkey 3
  130.  
  131. const PLR_SELECTOR = '#movie_player video, video#player_html5_api, video#player, video#video, video#vjs_video_3_html5_api';
  132.  
  133. const getPlayer = function() {
  134. return new Promise(resolve => {
  135. let plTimer = window.setInterval(() => {
  136. const plr = document.body.querySelector(PLR_SELECTOR);
  137. if (!!plr && plr.readyState >= 3) {
  138. window.clearInterval(plTimer);
  139. resolve(plr);
  140. }
  141. }, 10);
  142. });
  143. };
  144.  
  145. let player = await getPlayer();
  146.  
  147. const poiNotification = {
  148. title: "Point of interest found!",
  149. text: "This video has a highlight segment at " + durationString(rPoi) + ".\nClick here to skip to it.\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
  150. onclick: () => player.currentTime = rPoi,
  151. silent: true,
  152. timeout: 5000,
  153. image: favicon,
  154. }
  155.  
  156. const pfunc = function(){
  157. if (s3settings.notifications && !!rPoi && player.currentTime < rPoi) {
  158. GM.notification(poiNotification);
  159. }
  160. };
  161.  
  162. if (!result.length) {
  163. if (s3settings.notifications && !!rPoi)
  164. {
  165. GM.notification(poiNotification);
  166. player.addEventListener('play', pfunc);
  167. }
  168. return;
  169. }
  170.  
  171. if (s3settings.notifications && window.self === window.top) {
  172. let ntxt = "";
  173. if (result.length === rBefore) {
  174. ntxt = "Received " + result.length;
  175. if (result.length > 1) {
  176. ntxt += " segments."
  177. } else {
  178. ntxt += " segment."
  179. }
  180. } else {
  181. ntxt = "Received " + rBefore + " segments, " + result.length + " after processed.";
  182. }
  183. let newDuration = result[0].videoDuration;
  184. for (let x = 0; x < result.length; x++)
  185. {
  186. newDuration -= result[x].segment[1] - result[x].segment[0];
  187. }
  188. ntxt += "\nDuration: " + durationString(newDuration);
  189. let noti = {
  190. title: "Skippable segments found!",
  191. text: ntxt + "\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
  192. silent: true,
  193. timeout: 5000,
  194. image: favicon,
  195. };
  196. if (!!rPoi)
  197. {
  198. noti.text = noti.text.replace("\n\u00AD\n", "\n\u00AD\nThis video has a highlight segment at " + durationString(rPoi) + ".\nClick here to skip to it.\n\u00AD\n");
  199. noti.onclick = () => player.currentTime = rPoi;
  200. }
  201. GM.notification(noti);
  202. }
  203. const vfunc = function() {
  204. if (location.hostname !== 'odysee.com' &&
  205. location.pathname.indexOf(videoId) === -1 && location.search.indexOf('v=' + videoId) === -1)
  206. {
  207. player.removeEventListener('timeupdate', vfunc);
  208. player.removeEventListener('play', pfunc);
  209. return;
  210. }
  211.  
  212. if (!player.paused && x < result.length && player.currentTime >= result[x].segment[0]) {
  213. if (player.currentTime < result[x].segment[1]) {
  214. player.currentTime = result[x].segment[1];
  215. if (s3settings.notifications) {
  216. GM.notification({
  217. title: "Skipped " + result[x].category.replace('music_offtopic','non-music').replace('selfpromo', 'self-promotion') + " segment",
  218. text: "Segment " + (x + 1) + " out of " + result.length + "\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
  219. silent: true,
  220. timeout: 5000,
  221. image: favicon,
  222. });
  223. }
  224. console.log("Skipping " + result[x].category + " segment (" + (x + 1) + " out of " + result.length + ") from " + result[x].segment[0] + " to " + result[x].segment[1]);
  225. }
  226. x++;
  227. } else if (player.currentTime < prevTime) {
  228. for (let s = 0; s < result.length; s++) {
  229. if (player.currentTime < result[s].segment[1]) {
  230. x = s;
  231. console.log("Next segment is " + s);
  232. break;
  233. }
  234. }
  235. }
  236. prevTime = player.currentTime;
  237. };
  238. player.addEventListener('timeupdate', vfunc);
  239. player.addEventListener('play', pfunc);
  240. }
  241.  
  242. function durationString(scs) {
  243. const durDate = new Date(0);
  244. durDate.setSeconds(scs);
  245. const durHour = Math.floor(durDate.getTime() / 1000 / 60 / 60);
  246. const durMin = durDate.getUTCMinutes();
  247. const durSec = durDate.getUTCSeconds();
  248.  
  249. return (durHour > 0 ? durHour + ':' : '') + (durHour === 0 || durMin > 9 ? durMin : '0' + durMin) + ':' + (durSec > 9 ? durSec : '0' + durSec);
  250. }
  251.  
  252. function processSegments(segments) {
  253. if (typeof segments === 'object') {
  254. let newSegments = [];
  255. let highlight = null;
  256. let hUpvotes = s3settings.upvotes - 1;
  257. for (let x = 0; x < segments.length; x++) {
  258. if (segments[x].category === "poi_highlight" && segments[x].votes > hUpvotes) {
  259. highlight = segments[x];
  260. hUpvotes = segments[x].upvotes;
  261. } else if (x > 0 && newSegments[newSegments.length - 1].segment[1] >= segments[x].segment[0] && newSegments[newSegments.length - 1].segment[1] < segments[x].segment[1] && segments[x].votes >= s3settings.upvotes) {
  262. newSegments[newSegments.length - 1].segment[1] = segments[x].segment[1];
  263. newSegments[newSegments.length - 1].category = "combined";
  264. console.log(x + " combined with " + (newSegments.length - 1));
  265. } else if (segments[x].votes < s3settings.upvotes || (x > 0 && newSegments[newSegments.length - 1].segment[1] >= segments[x].segment[0] && newSegments[newSegments.length - 1].segment[1] >= segments[x].segment[1])) {
  266. console.log("Ignoring segment " + x);
  267. } else {
  268. newSegments.push(segments[x]);
  269. console.log((newSegments.length - 1) + " added");
  270. }
  271. }
  272. if (!!highlight)
  273. newSegments.push(highlight);
  274. return newSegments;
  275. } else {
  276. return [];
  277. }
  278. }
  279.  
  280. async function sha256(message) {
  281. // encode as UTF-8
  282. const msgBuffer = new TextEncoder().encode(message);
  283.  
  284. // hash the message
  285. const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
  286.  
  287. // convert ArrayBuffer to Array
  288. const hashArray = Array.from(new Uint8Array(hashBuffer));
  289.  
  290. // convert bytes to hex string
  291. const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  292. return hashHex;
  293. }
  294.  
  295. function shuffle(array) {
  296. let currentIndex = array.length, randomIndex;
  297.  
  298. // While there remain elements to shuffle.
  299. while (currentIndex != 0) {
  300.  
  301. // Pick a remaining element.
  302. randomIndex = Math.floor(Math.random() * currentIndex);
  303. currentIndex--;
  304.  
  305. // And swap it with the current element.
  306. [array[currentIndex], array[randomIndex]] = [
  307. array[randomIndex], array[currentIndex]];
  308. }
  309.  
  310. return array;
  311. }
  312.  
  313. let s3settings;
  314.  
  315. s3settings = await GM.getValue('s3settings');
  316. if(!!s3settings && Object.keys(s3settings).length > 0){
  317. console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Settings loaded!');
  318.  
  319. const isInt = (value) => {
  320. return !isNaN(value) &&
  321. parseInt(Number(value)) == value &&
  322. !isNaN(parseInt(value, 10));
  323. }
  324.  
  325. if (isInt(s3settings.categories)) { // converts enum categories to string array
  326. const cat = [];
  327. if (s3settings.categories & 2)
  328. cat.push("intro");
  329. if (s3settings.categories & 4)
  330. cat.push("outro");
  331. if (s3settings.categories & 8)
  332. cat.push("interaction");
  333. if (s3settings.categories & 16)
  334. cat.push("selfpromo");
  335. if (s3settings.categories & 32)
  336. cat.push("preview");
  337. if (s3settings.categories & 64)
  338. cat.push("music_offtopic");
  339. if (s3settings.categories & 128)
  340. cat.push("filler");
  341. if ((s3settings.categories & 1) || cat.length === 0)
  342. cat.push("sponsor");
  343. if (s3settings.notifications)
  344. cat.push("poi_highlight");
  345.  
  346. s3settings.categories = cat;
  347. await GM.setValue('s3settings', s3settings);
  348. }
  349. } else {
  350. s3settings = { "categories":["preview","sponsor","outro","music_offtopic","selfpromo","poi_highlight","interaction","intro"], "upvotes":-2, "notifications":true, "disable_hashing":false, "instance":"sponsor.ajay.app", "darkmode":-1 };
  351. if(navigator.userAgent.toLowerCase().indexOf('pale moon') !== -1
  352. || navigator.userAgent.toLowerCase().indexOf('mypal') !== -1
  353. || navigator.userAgent.toLowerCase().indexOf('male poon') !== -1)
  354. {
  355. s3settings.disable_hashing = true;
  356. }
  357. await GM.setValue('s3settings', s3settings);
  358. console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Default settings saved!');
  359. GM.notification({
  360. title: "Simple Sponsor Skipper",
  361. text: "It looks like this is your first time using Simple Sponsor Skipper.\n\u00AD\nClick here to open the configuration menu!",
  362. timeout: 10000,
  363. silent: true,
  364. onclick: function() { GM.openInTab(document.location.protocol + "//" + document.location.host.replace('youtube-nocookie.com', 'youtube.com') + document.location.pathname.replace('/embed/','/watch?v=').replace('/v/','/watch?v=') + document.location.search.replace('?','&').replace('&v=','?v=') + "#s3config"); },
  365. });
  366. }
  367. if (location.hash.toLowerCase() === '#s3config') {
  368. let loadevent = "DOMContentLoaded";
  369. if (location.hostname === "odysee.com")
  370. loadevent = "load";
  371.  
  372. window.addEventListener(loadevent, function() {
  373. const docHtml = document.getElementsByTagName('html')[0];
  374. docHtml.innerHTML = '\<center><h1>Simple Sponsor Skipper</h1><br><form><div><input type="checkbox" id="sponsor"><label for="sponsor">Skip sponsor segments</label><br><input type="checkbox" id="intro"><label for="intro">Skip intro segments</label><br><input type="checkbox" id="outro"><label for="outro">Skip outro segments</label><br><input type="checkbox" id="interaction"><label for="interaction">Skip interaction reminder segments</label><br><input type="checkbox" id="selfpromo"><label for="selfpromo">Skip self-promotion segments</label><br><input type="checkbox" id="preview"><label for="preview">Skip preview segments</label><br><input type="checkbox" id="music_offtopic"><label for="music_offtopic">Skip non-music segments in music videos</label><br><input type="checkbox" id="filler"><label for="filler">Skip filler segments (WARNING: very aggressive!)</label><br><label for="upvotes">Minimum segment upvotes:</label><input type="number" id="upvotes"><br><input type="checkbox" id="notifications"><label for="notifications">Enable Desktop Notifications</label><br><input type="checkbox" id="disable_hashing"><label for="disable_hashing">Disable Video ID Hashing (Pale Moon Compatibility Fix)</label><br><label for="instance">Database Instance:</label><input id="instance" type="text" list="instances" /><datalist id="instances"><option value="sponsor.ajay.app">sponsor.ajay.app (Official)</option><option value="sponsorblock.kavin.rocks">sponsorblock.kavin.rocks</option><option value="sponsorblock.gleesh.net">sponsorblock.gleesh.net</option><option value="sb.theairplan.com">sb.theairplan.com</option></datalist><br><label for="darkmode">Theme:</label><select id="darkmode"><option value="-1">auto</option><option value="0">light</option> <option value="1">dark</option></select></div><br><div><button type="button" id="btnsave" style="margin-right: 1em;">Save settings</button><button type="button" id="btnclose" style="margin-left: 1em;">Close</button></div></form></center>';
  375. docHtml.style = "";
  376. document.head.innerHTML = "\<style> body { background-color: white; color: black; } .dark-theme { background-color: black; color: white; } </style>";
  377. document.title = 'Simple Sponsor Skipper Configuration';
  378. document.getElementById('sponsor').checked = s3settings.categories.includes("sponsor");
  379. document.getElementById('intro').checked = s3settings.categories.includes("intro");
  380. document.getElementById('outro').checked = s3settings.categories.includes("outro");
  381. document.getElementById('interaction').checked = s3settings.categories.includes("interaction");
  382. document.getElementById('selfpromo').checked = s3settings.categories.includes("selfpromo");
  383. document.getElementById('preview').checked = s3settings.categories.includes("preview");
  384. document.getElementById('music_offtopic').checked = s3settings.categories.includes("music_offtopic");
  385. document.getElementById('filler').checked = s3settings.categories.includes("filler");
  386. document.getElementById('upvotes').value = s3settings.upvotes;
  387. document.getElementById('notifications').checked = s3settings.notifications;
  388. document.getElementById('disable_hashing').checked = s3settings.disable_hashing;
  389. document.getElementById('instance').value = s3settings.instance || "sponsor.ajay.app";
  390. document.getElementById('darkmode').value = s3settings.darkmode || -1;
  391. document.getElementById('darkmode').addEventListener("change", function(e) {
  392. const val = parseInt(e.target.value, 10);
  393. if (val === 1 ||
  394. (val === -1 && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches))
  395. {
  396. document.body.classList.add('dark-theme');
  397. }
  398. else { document.body.classList.remove('dark-theme'); }
  399. });
  400. document.getElementById('darkmode').dispatchEvent(new Event('change'));
  401.  
  402. const btnSave = document.getElementById('btnsave');
  403. btnSave.addEventListener("click", async function() {
  404.  
  405. // segment categories
  406. s3settings.categories = [];
  407. if (document.getElementById('sponsor').checked) s3settings.categories.push("sponsor");
  408. if (document.getElementById('intro').checked) s3settings.categories.push("intro");
  409. if (document.getElementById('outro').checked) s3settings.categories.push("outro");
  410. if (document.getElementById('interaction').checked) s3settings.categories.push("interaction");
  411. if (document.getElementById('selfpromo').checked) s3settings.categories.push("selfpromo");
  412. if (document.getElementById('preview').checked) s3settings.categories.push("preview");
  413. if (document.getElementById('music_offtopic').checked) s3settings.categories.push("music_offtopic");
  414. if (document.getElementById('filler').checked) s3settings.categories.push("filler");
  415. else if (s3settings.categories.length === 0) s3settings.categories = ["sponsor"];
  416. if (document.getElementById('notifications').checked) s3settings.categories.push("poi_highlight");
  417. // end
  418.  
  419. s3settings.upvotes = parseInt(document.getElementById('upvotes').value, 10) || -2;
  420. s3settings.notifications = document.getElementById('notifications').checked;
  421. s3settings.disable_hashing = document.getElementById('disable_hashing').checked;
  422. if (document.getElementById('instance').value.trim() != "")
  423. s3settings.instance = document.getElementById('instance').value.trim();
  424. s3settings.darkmode = parseInt(document.getElementById('darkmode').value, 10);
  425. await GM.setValue('s3settings', s3settings);
  426. console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Settings saved!');
  427. btnSave.textContent = "Saved!";
  428. btnSave.disabled = true;
  429. setTimeout(() => { btnSave.textContent = "Save settings"; btnSave.disabled = false; }, 3000);
  430. });
  431. document.getElementById('btnclose').addEventListener("click", function() {
  432. location.replace(location.protocol + "//" + location.host + location.pathname + location.search)
  433. });
  434. });
  435. } else {
  436. let oldVidId = "";
  437. let params = new URLSearchParams(location.search);
  438. if (params.has('v')) {
  439. oldVidId = params.get('v');
  440. go(oldVidId);
  441. } else if (location.pathname.indexOf('/embed/') === 0 || location.pathname.indexOf('/v/') === 0) {
  442. oldVidId = location.pathname.replace('/v/', '').replace('/embed/', '').split('/')[0];
  443. go(oldVidId);
  444. }
  445.  
  446. window.addEventListener("load", function() {
  447. let observer = new MutationObserver(function(mutations) {
  448. if (location.hostname === "odysee.com")
  449. {
  450. mutations.forEach(function(mutation) {
  451. for (let x = 0; x < mutation.addedNodes.length; x++) {
  452. if (!mutation.addedNodes[x].tagName)
  453. continue;
  454.  
  455. if (mutation.addedNodes[x].id === "vjs_video_3")
  456. {
  457. let thumb = document.body.querySelector('div.content__cover');
  458. if (!!thumb) {
  459. thumb = thumb.style.backgroundImage;
  460. thumb = thumb.substring(thumb.indexOf('\"') + 1).split('\"')[0];
  461. if(thumb.indexOf('ytimg.com') !== -1 || thumb.indexOf('img.youtube.com') !== -1){
  462. go(thumb.split('/vi/').pop().split('/')[0]);
  463. } else if (!thumb.toLowerCase().match(/\.(webp|jpeg|jpg|gif|png)$/)) {
  464. go(thumb.split('/').pop());
  465. }
  466. }
  467. break;
  468. }
  469. }
  470. });
  471. }
  472. else
  473. {
  474. params = new URLSearchParams(location.search);
  475. if (params.has('v') && params.get('v') !== oldVidId) {
  476. oldVidId = params.get('v');
  477. go(oldVidId);
  478. } else if ((location.pathname.indexOf('/embed/') === 0 || location.pathname.indexOf('/v/') === 0) && location.pathname.indexOf(oldVidId) === -1) {
  479. oldVidId = location.pathname.replace('/v/', '').replace('/embed/', '').split('/')[0];
  480. go(oldVidId);
  481. } else if (!params.has('v') && location.pathname.indexOf('/embed/') === -1 && location.pathname.indexOf('/v/') === -1) {
  482. oldVidId = "";
  483. }
  484. }
  485. });
  486.  
  487. let config = {
  488. childList: true,
  489. subtree: true
  490. };
  491.  
  492. observer.observe(document.body, config);
  493. });
  494. }
  495. if (window.self === window.top) {
  496. GM.registerMenuCommand("Configuration", function() { window.location.replace(window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search + "#s3config"); window.location.reload(); });
  497. }
  498. })();