diff --git a/web/userscripts/InviReg.user.js b/web/userscripts/InviReg.user.js
index 2c47ff5..fe57830 100644
--- a/web/userscripts/InviReg.user.js
+++ b/web/userscripts/InviReg.user.js
@@ -4,12 +4,12 @@
// @namespace https://gist.github.com/Albirew/
// @description The smallest, possibly most useful YouTube-Invidious conversion script. RegEx based. Sends any /www.youtube.com/ sites to Invidious before page load. Does not affect /other.youtube.com/, (gaming.youtube.com, creatoracademy.youtube.com, etc.).
// @include *youtube*
-// @version 1.1c
+// @version 1.1d
// @icon https://invidious.fdn.fr/favicon.ico
// @grant none
-// @run-at document-start
-// @downloadURL https://gist.githubusercontent.com/Albirew/4bb3bee6a8037b44cf67521284e94b93/raw/InviReg.user.js
-// @updateURL https://gist.githubusercontent.com/Albirew/4bb3bee6a8037b44cf67521284e94b93/raw/InviReg.user.js
+// @run-at document-start
+// @downloadURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/InviReg.user.js
+// @updateURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/InviReg.user.js
// ==/UserScript==
var url = window.location.toString();
diff --git a/web/userscripts/Let-s-panda.user.js b/web/userscripts/Let-s-panda.user.js
new file mode 100644
index 0000000..6443ffd
--- /dev/null
+++ b/web/userscripts/Let-s-panda.user.js
@@ -0,0 +1,1231 @@
+// ==UserScript==
+// @name Let's panda!
+// @namespace https://github.com/Sean2525/Let-s-panda
+// @author sean2525, strong-Ting
+// @description A login, view, download tool for exhentai & e-hentai
+// @description:zh-tw 一個用於exhentai和e-hentai的登入、查看、下載的工具
+// @description:zh-cn 一个用于exhentai和e-hentai的登录、查看、下载的工具
+// @license MIT
+// @require https://code.jquery.com/jquery-3.2.1.slim.min.js
+// @include https://exhentai.org/
+// @include https://exhentai.org/g/*
+// @include https://e-hentai.org/g/*
+// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
+// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
+// @grant GM_xmlhttpRequest
+// @grant GM.xmlHttpRequest
+// @grant GM_setValue
+// @grant GM_getValue
+// @grant GM.setValue
+// @grant GM.getValue
+// @grant GM_notification
+// @grant GM.notification
+// @connect *
+// @run-at document-end
+// @version 0.2.17
+// ==/UserScript==
+
+jQuery(function ($) {
+ /**
+ * Output extension
+ * @type {String} zip
+ * cbz
+ *
+ * Tips: Convert .zip to .cbz
+ * Windows
+ * $ ren *.zip *.cbz
+ * Linux
+ * $ rename 's/\.zip$/\.cbz/' *.zip
+ */
+ var outputExt = "zip"; // or 'cbz'
+
+ /**
+ * Multithreading
+ * @type {Number} [1 -> 32]
+ */
+ var threading = 8;
+
+ /**
+ * Logging
+ * @type {Boolean}
+ */
+ var debug = false;
+
+
+ var viewed = false;
+ const loginPage = () => {
+ let div = document.createElement("div");
+ div.className = "main";
+ let username = document.createElement("input");
+ let style = document.createElement("style");
+ style.innerHTML = `
+body {
+ background-color: #212121;
+}
+.main {
+display: -webkit-flex;
+display: flex;
+-webkit-flex-direction: column;
+flex-direction: column;
+-webkit-align-items: center;
+align-items: center;
+-webkit-justify-content: center;
+justify-content: center;
+height: ${window.innerHeight}px;
+}
+.flex-center{
+display: -webkit-flex;
+display: flex;
+-webkit-align-items: center;
+align-items: center;
+-webkit-justify-content: center;
+justify-content: center;
+}
+form {
+display: -webkit-flex;
+display: flex;
+-webkit-flex-direction: column;
+flex-direction: column;
+-webkit-align-items: center;
+align-items: center;
+-webkit-justify-content: center;
+justify-content: center;
+}
+.image {
+position: relative;
+margin: 0;
+}
+.input {
+margin-top: 10px;
+display: block;
+height: 34px;
+padding: 6px 12px;
+font-size: 14px;
+line-height: 1.42857143;
+color: #555;
+background-color: #fff;
+background-image: none;
+border: 1px solid #ccc;
+border-radius: 4px;
+-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
+-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+}
+.btn {
+color: #fff;
+background-color: #5cb85c;
+border-color: #4cae4c;
+margin-top: 10px;
+display: inline-block;
+font-weight: 400;
+line-height: 1.25;
+text-align: center;
+white-space: nowrap;
+vertical-align: middle;
+-webkit-user-select: none;
+-moz-user-select: none;
+-ms-user-select: none;
+user-select: none;
+border: 1px solid transparent;
+padding: .5rem 1rem;
+font-size: 1rem;
+border-radius: .25rem;
+-webkit-transition: all .2s ease-in-out;
+-o-transition: all .2s ease-in-out;
+transition: all .2s ease-in-out;
+}
+.btn:hover {
+ background-color: #4da64d;
+}
+.btn-blue {
+ color: #fff;
+ background-color: #3832dd;
+ border-color: #3832dd;
+ display: inline-block;
+ font-weight: 400;
+ line-height: 1.0;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ border: 1px solid transparent;
+ padding: .5rem 1rem;
+ font-size: 1rem;
+ border-radius: .25rem;
+ -webkit-transition: all .2s ease-in-out;
+ -o-transition: all .2s ease-in-out;
+ transition: all .2s ease-in-out;
+ }
+.btn-blue:hover {
+ background-color: #1c15c8;
+}
+`;
+ $("head").append(style);
+ const setCookie = (headers) => {
+ //
+ try {
+ headers
+ .split("\r\n")
+ .find((x) => x.match("cookie"))
+ .replace("set-cookie: ", "")
+ .split("\n")
+ .map(
+ (x) =>
+ (document.cookie = x.replace(".e-hentai.org", ".exhentai.org") + " secure")
+ );
+ } catch (err) {
+ if (debug) console.log(err);
+ }
+ document.cookie =
+ "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";
+
+ setTimeout(function () { window.location.reload() }, 3000);
+ };
+ const clearCookie = () => {
+ if (debug) console.log("Clearning cookies");
+ document.cookie =
+ "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";
+ window.location.reload();
+ };
+ let form = document.createElement("form");
+ let login = document.createElement("button");
+ let wrapper = document.createElement("div");
+ let loadding = document.createElement("img");
+ let password = document.createElement("input");
+ username.placeholder = "Username"
+ password.placeholder = "Password"
+ let info = document.createElement("p");
+ let error = document.createElement("p");
+ info.innerHTML = `
+
+ If you can't log in, please visit the Forums and log in from there.
+Please make sure you are logged in successfully and then click this
+
+`;
+ info.style.color = "white";
+ username.type = "text";
+ username.className = "input";
+ password.type = "password";
+ password.className = "input";
+ loadding.src =
+ "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==";
+ loadding.style.position = "relative";
+ info.hidden = true;
+ loadding.hidden = true;
+ login.addEventListener("click", () => {
+ loadding.hidden = false;
+ GM.xmlHttpRequest({
+ method: "POST",
+ url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01",
+ data: `referer=https://forums.e-hentai.org/index.php?&b=&bt=&UserName=${username.value}&PassWord=${password.value}&CookieDate=1"}`,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ onload: function (response) {
+ if (debug) console.log(response);
+ if (/You are now logged/.exec(response.responseText)) {
+ error.style = "color:green";
+ error.innerText = "Login succeeded: you will be redirected to exhentai.org in 3 seconds, if you can't access exhentai, don't use private browsing. "
+ GM.notification("You will be redirected to exhentai.org in 3 seconds; if you can't access exhentai, don't use private browsing", "Login succeeded");
+ setCookie(response.responseHeaders);
+ } else if (/IF YOU DO NOT SEE THE CAPTCHA/.exec(response.responseText)) {
+ error.style = "color:red";
+ error.innerText = "Login failed: Please visit the forums directly and log in from there; reCaptcha has been enabled."
+ }
+ else {
+ error.style = "color:red";
+ error.innerText = "Login failed: Please check that your username and password are correct.";
+ }
+ info.hidden = false;
+ loadding.hidden = true;
+ },
+ onerror: function (err) {
+ console.error(err);
+ error.style = "color:red";
+ error.innerText("Login got error: Please contact me at https://github.com/MinoLiu/Let-s-panda/issues");
+ loadding.hidden = true;
+ },
+ });
+ });
+ login.className = "btn";
+ login.innerHTML = "Login";
+ form.append(username);
+ form.append(password);
+ wrapper.className = "flex-center";
+ wrapper.append(loadding);
+ wrapper.append(login);
+ form.append(wrapper);
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ });
+ var image = document.createElement("img");
+ image.className = "image";
+ image.src = "https://i.imgur.com/oX86mGf.png"
+ div.append(image);
+ div.append(form);
+ div.append(error);
+ div.append(info);
+ $("body").append(div);
+ $(".clearCookie").on("click", clearCookie);
+ };
+
+ const downloadPage = () => {
+ var zip = new JSZip(),
+ doc = document,
+ tit = doc.title,
+ $win = $(window),
+ loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(doc.location.href)[0],
+ prevZip = false,
+ current = 0,
+ images = [],
+ total = 0,
+ final = 0,
+ failed = 0,
+ hrefs = [],
+ comicId = location.pathname.match(/\d+/)[0],
+ download = document.createElement("p");
+
+ const dlImg = ({ index, url, _ }, success, error) => {
+ var filename = url.replace(/.*\//g, "");
+ var extension = filename.split(".").pop();
+ filename = ("0000" + index).slice(-4) + "." + extension;
+ if (debug) console.log(filename, "progress");
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: url,
+ responseType: "arraybuffer",
+ onload: function (response) {
+ final++;
+ success(response, filename);
+ },
+ onerror: function (err) {
+ final++;
+ error(err, filename);
+ },
+ });
+ };
+
+ const next = () => {
+ download.innerHTML = `▶ Downloading ${final}/${total}`;
+ if (debug) console.log(final, current);
+ if (final < current) return;
+ final < total ? addZip() : genZip();
+ };
+
+ const end = () => {
+ $win.off("beforeunload");
+ if (failed > 0) {
+ alert("Some pages download failed, please unzip and check!");
+ }
+ if (debug) console.timeEnd("eHentai");
+ };
+
+ const genZip = () => {
+ zip
+ .generateAsync({
+ type: "blob",
+ })
+ .then(function (blob) {
+ var zipName =
+ tit.replace(/\s/g, "_") + "." + comicId + "." + outputExt;
+
+ if (prevZip) window.URL.revokeObjectURL(prevZip);
+ prevZip = blob;
+
+ saveAs(blob, zipName);
+ if (debug) console.log("COMPLETE");
+ download.innerHTML = `▶ Download completed!`;
+ end();
+ });
+ };
+
+ const addZip = () => {
+ total = images.length;
+ var max = current + threading;
+ if (max > total) max = total;
+ for (current; current < max; current++) {
+ let _href = images[current];
+ dlImg(
+ _href,
+ function (response, filename) {
+ zip.file(filename, response.response);
+ if (debug) console.log(filename, "image success");
+ next();
+ },
+ function (err, filename) {
+ final--;
+ // retry backupUrl for once
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: _href.backupUrl,
+ onload: function (response) {
+ let imgNo = parseInt(
+ response.responseText.match("startpage=(\\d+)").pop()
+ );
+ let img = new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelector("#img");
+ if (debug) console.log(imgNo, "backupUrl success");
+ _href.url = img.src;
+ dlImg(
+ _href,
+ function (response, filename) {
+ zip.file(filename, response.response);
+ if (debug) console.log(filename, "backupUrl image success");
+ next();
+ },
+ function (err, filename) {
+ failed++;
+ zip.file(
+ filename + "_" + comicId + "_error.gif",
+ "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
+ {
+ base64: true,
+ }
+ );
+ if (debug) console.log(filename, "backupUrl image error");
+ next();
+ }
+ );
+ },
+ onerror: function (err, filename) {
+ dlImg(
+ _href,
+ function (response, filename) {
+ zip.file(filename, response.response);
+ if (debug) console.log(filename, "retry image success");
+ next();
+ },
+ function (err, filename) {
+ failed++;
+ zip.file(
+ filename + "_" + comicId + "_error.gif",
+ "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
+ {
+ base64: true,
+ }
+ );
+ if (debug) console.log(filename, "retry url error");
+ next();
+ }
+ );
+ }
+ });
+ }
+ );
+ }
+ };
+
+ /**
+ * Update image download status.
+ */
+ const getImageNext = () => {
+ download.innerHTML = `▶ Getting images ${final}/${hrefs.length}`;
+ if (debug) console.log(final, current);
+ if (final < current) return;
+ final < hrefs.length
+ ? getImage()
+ : (() => {
+ current = 0;
+ final = 0;
+ addZip();
+ })();
+ };
+
+ /**
+ * Get all images from hrefs.
+ */
+ const getImage = () => {
+ let max = current + threading;
+ if (max > hrefs.length) max = hrefs.length;
+ for (current; current < max; current++) {
+ if (debug) console.log(hrefs[current]);
+ let href = hrefs[current];
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: hrefs[current],
+ onload: function (response) {
+ let imgNo = parseInt(
+ response.responseText.match("startpage=(\\d+)").pop()
+ );
+ let img = new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelector("#img");
+ if (debug) console.log(imgNo, "url success");
+ let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
+ images.push({
+ index: imgNo,
+ url: img.src,
+ backupUrl: src,
+ });
+ final++;
+ getImageNext();
+ },
+ onerror: function (err) {
+ final++;
+ getImageNext();
+ if (debug) console.log(err);
+ },
+ });
+ }
+ };
+
+ /**
+ * Get the href of all images from all pages.
+ */
+ const getHref = () => {
+ childNodes = document.querySelector("table[class=ptt] tbody tr")
+ .childNodes;
+ let page = parseInt(
+ childNodes[childNodes.length - 2].textContent.replace(",", "")
+ );
+ for (let i = 0; i < page; i++) {
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: `${loc}?p=${i}`,
+ onload: function (response) {
+ if (debug)
+ console.log(`page ${loc}?p=${i} detect ${response.responseText}`);
+ let imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtm a"),
+ ];
+ if (!imgs.length)
+ imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtl a"),
+ ];
+ if (!imgs.length) {
+ alert(
+ "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
+ );
+ }
+ imgs.forEach((v) => {
+ hrefs.push(v.href);
+ });
+ if (i == page - 1) {
+ getImage();
+ }
+ },
+ onerror: function (err) {
+ download.innerHTML =
+ '▶ Get href failed';
+ if (i == page - 1) {
+ getImage();
+ }
+ if (debug) console.log(err);
+ },
+ });
+ }
+ };
+
+ download.className = "g3";
+ download.innerHTML = `▶ Download`;
+ $("#gd5").append(download);
+ $(".panda_download").on("click", () => {
+ if (threading < 1) threading = 1;
+ if (threading > 32) threading = 32;
+ if (debug) console.time("eHentai");
+ $win.on("beforeunload", function () {
+ return "Progress is running...";
+ });
+ download.innerHTML = `▶ Start Download`;
+ getHref();
+ });
+ };
+
+
+ function view() {
+ viewed = true;
+ if (threading < 1) threading = 1;
+ if (threading > 32) threading = 32;
+ var gdt = document.querySelector("#gdt");
+ var gdd = document.querySelector("#gdd");
+ var gdo4 = document.createElement("div");
+ gdo4.setAttribute("id", "gdo4");
+ $("body").append(gdo4);
+
+ let childNodes = document.querySelector("table[class=ptt] tbody tr")
+ .childNodes;
+ let lpPage = parseInt(
+ childNodes[childNodes.length - 2].textContent.replace(",", "")
+ );
+
+ var data = document
+ .querySelector("body div.gtb p.gpc")
+ .textContent.split(" ");
+
+ var minPic = parseInt(data[1].replace(",", ""));
+ var maxPic = parseInt(data[3].replace(",", ""));
+
+ var imgNum = parseInt(
+ gdd
+ .querySelector("#gdd tr:nth-child(n+6) td.gdt2")
+ .textContent.split(" ")[0]
+ );
+
+
+ viewer(lpPage, imgNum, minPic, maxPic);
+
+ async function viewer(lpPage, imgNum, minPic, maxPic) {
+ var Gallery = function (pageNum, imgNum, minPic, maxPic) {
+ this.pageNum = pageNum || 0;
+ this.imgNum = imgNum || 0;
+ this.loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(location.href)[0];
+ this.padding = false;
+ this.current = 0;
+ this.final = 0;
+ };
+ var viewAll = await GM.getValue("view_all", true);
+ Gallery.prototype = {
+ imgHref: [],
+ imgList: [],
+ retry: 0,
+ getAllHref: function (nextID) {
+ if (nextID >= this.pageNum) {
+ this.loadNextImage();
+ return;
+ }
+ var that = this;
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: `${this.loc}?p=${nextID}`,
+ onload: function (response) {
+ if (debug)
+ console.log(`page ${that.loc}?p=${nextID} detect ${response.responseText}`);
+ let imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtm a"),
+ ];
+ if (!imgs.length)
+ imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtl a"),
+ ];
+ if (!imgs.length) {
+ alert(
+ "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
+ );
+ }
+ imgs.forEach((v) => {
+ that.imgHref.push(v.href);
+ });
+ that.getAllHref(nextID + 1);
+ },
+ onerror: function (err) {
+ if (debug) console.log(err);
+ that.retry++;
+ if (that.retry > 2) {
+ alert(`Page number ${nextID + 1} load failed for 3 times.`);
+ that.getAllHref(nextID + 1);
+ } else {
+ that.getAllHref(nextID);
+ }
+ },
+ });
+ },
+ getHref: function (pageID) {
+ var that = this;
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: `${this.loc}?p=${pageID}`,
+ onload: function (response) {
+ if (debug)
+ console.log(`page ${that.loc}?p=${pageID} detect ${response.responseText}`);
+ let imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtm a"),
+ ];
+ if (!imgs.length)
+ imgs = [
+ ...new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelectorAll(".gdtl a"),
+ ];
+ if (!imgs.length) {
+ alert(
+ "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
+ );
+ }
+ imgs.forEach((v) => {
+ that.imgHref.push(v.href);
+ });
+ that.loadNextImage();
+ },
+ onerror: function (err) {
+ if (debug) console.log(err);
+ that.retry++;
+ if (that.retry > 2) {
+ alert(`Page number ${nextID + 1} load failed for 3 times.`);
+ that.loadNextImage();
+ } else {
+ that.getHref(nextID);
+ }
+ },
+ });
+ },
+ checkFunctional: function () {
+ return (this.imgNum > 41 && this.pageNum < 2) || this.imgNum !== 0;
+ },
+ loadNextImage: function () {
+ if (this.final < this.current) {
+ return;
+ }
+ this.loadPageUrls();
+ },
+ onSucceed: async function (response, href) {
+ let imgNo = parseInt(
+ response.responseText.match("startpage=(\\d+)").pop()
+ );
+ let img = new DOMParser()
+ .parseFromString(response.responseText, "text/html")
+ .querySelector("#img");
+ if (debug) console.log(imgNo, "success");
+ let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
+ Gallery.prototype.imgList[imgNo - 1].setAttribute(
+ "data-href",
+ src
+ );
+
+ let timeoutId;
+ let timeoutDuration = 10000; // 10s
+
+ timeoutId = setTimeout(function () {
+ // timeout trigger error
+ Gallery.prototype.imgList[imgNo - 1].childNodes[0].dispatchEvent(new Event('error'));
+ }, timeoutDuration);
+
+ $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on("load", function () {
+ // success clear timeoutId
+ clearTimeout(timeoutId);
+ });
+
+ $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on(
+ "error",
+ function () {
+ var ajax = new XMLHttpRequest();
+ ajax.onreadystatechange = async function () {
+ if (debug) {
+ console.log(`Failed load ${Number(imgNo)}, getting backup image from ${src}.`);
+ }
+ if (4 == ajax.readyState && 200 == ajax.status) {
+ var _imgNo = parseInt(
+ ajax.responseText.match("startpage=(\\d+)").pop()
+ );
+ var imgDom = new DOMParser()
+ .parseFromString(ajax.responseText, "text/html")
+ .getElementById("img");
+ Gallery.prototype.imgList[_imgNo - 1].childNodes[0].src =
+ imgDom.src;
+ }
+ };
+ ajax.open("GET", src);
+ ajax.send(null);
+ }
+ );
+
+ Gallery.prototype.imgList[imgNo - 1].childNodes[0].src = img.src;
+
+ this.loadNextImage();
+ },
+ onFailed: function (err, href) {
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: href,
+ responseType: "document",
+ onload: function (response) {
+ that.onSucceed(response, href);
+ },
+ onerror: function (err) {
+ if (debug) console.log(err);
+ this.loadNextImage();
+ },
+ });
+ },
+ loadPageUrls: function () {
+ if (debug) {
+ console.log("load work");
+ }
+ let max = threading + this.current > this.imgHref.length ? this.imgHref.length : threading + this.current;
+ for (this.current; this.current < max; this.current++) {
+ let that = this;
+ let href = this.imgHref[this.current];
+ GM.xmlHttpRequest({
+ method: "GET",
+ url: href,
+ responseType: "document",
+ onload: function (response) {
+ that.final++;
+ that.onSucceed(response, href);
+ },
+ onerror: function (err) {
+ if (debug) console.log(err);
+ that.final++;
+ that.onFailed(err, href);
+ },
+ });
+ }
+ },
+ cleanGDT: function () {
+ while (gdt.firstChild && gdt.firstChild.className)
+ gdt.removeChild(gdt.firstChild);
+ },
+
+ generateImg: function (callback) {
+ for (var i = 0; i < this.imgNum; i++) {
+ if (i < maxPic && i >= minPic - 1) {
+ var img = document.createElement("img");
+ var a = document.createElement("a");
+ img.setAttribute("src", "https://ehgt.org/g/roller.gif");
+ img.setAttribute("loadding", "lazy");
+ a.appendChild(img);
+ this.imgList.push(a);
+
+ gdt.appendChild(a);
+ } else {
+ var img = document.createElement("img");
+ var a = document.createElement("a");
+
+ img.setAttribute("src", "https://ehgt.org/g/roller.gif");
+ img.setAttribute("loadding", "lazy");
+ a.appendChild(img);
+
+ this.imgList.push(a);
+ if (viewAll) gdt.appendChild(a);
+ }
+ }
+
+ gdt.style.textAlign = "center";
+ gdt.style.maxWidth = "100%";
+
+ gdo4.innerHTML = ""; //clear origin button(Normal Large)
+
+ var style = document.createElement("style");
+ style.type = "text/css";
+ style.innerHTML = `
+div#gdo4{
+position:fixed;
+width: 212px;
+height:32px;
+left:unset;
+right:10px;
+bottom:0px;
+top:unset;
+text-align:right;
+z-index:1;
+background:#34353b;
+border-radius:5%;
+}
+
+
+
+
+.double {
+font-weight: bold;
+// margin: 0 2px 4px 2px;
+float: left;
+border-radius: 5px;
+height:32px;
+width: 32px;
+//border: 1px solid #989898;
+//background: #4f535b;
+background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
+}
+
+.double:hover{
+background: #4f535b;
+background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
+}
+
+.single{
+font-weight: bold;
+// margin: 0 2px 4px 2px;
+float: left;
+border-radius: 5px;
+height:32px;
+width: 32px;
+//border: 1px solid #989898;
+// background: #4f535b;
+background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);
+}
+
+.size_pic{
+font-weight: bold;
+// margin: 0 2px 4px 2px;
+float: left;
+border-radius: 2px;
+height:16px;
+width: 16px;
+//border: 1px solid #989898;
+// background: #4f535b;
+}
+
+.single:hover{
+background: #4f535b;
+background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);
+
+}
+
+.size_btn {
+height: 32px;
+width: 32px;
+border-radius: 100%;
+//font-family: Arial;
+color: #ffffff;
+font-size: 16px;
+background: #4f535b;
+text-decoration: none;
+}
+
+
+.pad_pic {
+height: 32px;
+width: 32px;
+border-radius: 100%;
+//font-family: Arial;
+color: #ffffff;
+font-size: 16px;
+background: #4f535b;
+text-decoration: none;
+}
+
+.size_btn:hover {
+background: #a9adb1;
+text-decoration: none;
+}
+`;
+ document.getElementsByTagName("head")[0].appendChild(style);
+
+ //show
+
+ var single_pic = document.createElement("div"); //create single button
+ single_pic.className = "single";
+ single_pic.innerHTML += "";
+ gdo4.appendChild(single_pic);
+
+ var double_pic = document.createElement("div"); //create double button
+ double_pic.className = "double";
+ double_pic.innerHTML = "";
+ gdo4.appendChild(double_pic);
+
+ var pad_pic = document.createElement("button");
+ pad_pic.className = "pad_pic";
+ pad_pic.innerHTML += "p";
+ gdo4.appendChild(pad_pic);
+
+ var full_pic = document.createElement("button");
+ full_pic.className = "pad_pic";
+ full_pic.innerHTML += "f";
+ gdo4.appendChild(full_pic);
+
+ var size_pic_reduce = document.createElement("button");
+ size_pic_reduce.className = "size_btn";
+ size_pic_reduce.innerHTML += "-";
+ gdo4.appendChild(size_pic_reduce);
+
+ var size_pic_add = document.createElement("button");
+ size_pic_add.className = "size_btn";
+ size_pic_add.innerHTML += "+";
+ gdo4.appendChild(size_pic_add);
+
+ document
+ .getElementById("gdo4")
+ .children[0] //when single button click change value of width
+ .addEventListener("click", async function (event) {
+ await GM.setValue("width", "0.7");
+ await GM.setValue("mode", "single");
+ await pic_width(await GM.getValue("width"));
+ $("wrap").remove();
+
+ wrap(await GM.getValue("width"));
+ });
+
+ document
+ .getElementById("gdo4")
+ .children[1] //when double button click change value of width
+ .addEventListener("click", async function (event) {
+ await GM.setValue("width", "0.49");
+ await GM.setValue("mode", "double");
+ let view_reverse = await GM.getValue("view_reverse", true);
+ GM.setValue("view_reverse", !view_reverse);
+ await pic_width(await GM.getValue("width"));
+ $("wrap").remove();
+
+ wrap(await GM.getValue("mode"));
+ });
+
+ var pad_img = document.createElement("img");
+ var pad_a = document.createElement("a");
+ pad_a.appendChild(pad_img);
+
+ document
+ .getElementById("gdo4")
+ .children[2].addEventListener("click", async (event) => {
+ this.padding = !this.padding;
+ const view_reverse = await GM.getValue("view_reverse", true);
+ await GM.setValue("view_reverse", false);
+ $("wrap").remove();
+ await wrap(await GM.getValue("mode"));
+ $("wrap").remove();
+ if (this.padding) {
+ this.imgList.unshift(pad_a);
+ gdt.insertBefore(pad_a, gdt.firstChild);
+ } else {
+ this.imgList.shift();
+ gdt.removeChild(pad_a);
+ }
+ await GM.setValue("view_reverse", view_reverse);
+ await wrap(await GM.getValue("mode"));
+ });
+
+ document
+ .getElementById("gdo4")
+ .children[3].addEventListener("click", async function (event) {
+ await GM.setValue("full_image", true);
+ await pic_width(0);
+ });
+
+ document
+ .getElementById("gdo4")
+ .children[4].addEventListener("click", async function (event) {
+ await GM.setValue("full_image", false);
+ var size_width = parseFloat(await GM.getValue("width"));
+ if (size_width > 0.2 && size_width < 1.5) {
+ size_width = size_width - 0.1;
+ GM.setValue("width", size_width);
+ }
+ let _width = await GM.getValue("width");
+ await pic_width(_width);
+ console.log(_width);
+ });
+
+ document
+ .getElementById("gdo4")
+ .children[5].addEventListener("click", async function (event) {
+ await GM.setValue("full_image", false);
+ var size_width = parseFloat(await GM.getValue("width"));
+ if (size_width > 0.1 && size_width < 1.4) {
+ size_width = size_width + 0.1;
+ GM.setValue("width", size_width);
+ }
+ let _width = await GM.getValue("width");
+ await pic_width(_width);
+ console.log(_width);
+ });
+
+ async function pic_width(
+ width //change width of pics
+ ) {
+ for (var i = maxPic - minPic + 1; i > 0; i--) {
+ await resizeImg(width);
+ }
+ }
+
+ callback && callback();
+ },
+ };
+ var g = new Gallery(lpPage, imgNum, minPic, maxPic);
+
+ if (g.checkFunctional()) {
+ var viewAll = await GM.getValue("view_all", true);
+ g.generateImg(function () {
+ if (g.pageNum && viewAll) {
+ g.getAllHref(0);
+ } else {
+ g.getHref(Number(document.querySelector("td.ptds").childNodes[0].text) - 1);
+ }
+ g.cleanGDT();
+ });
+
+ document.addEventListener("keydown", (e) => {
+ let nextImg = null;
+
+ if (e.code === "ArrowUp") {
+ for (let i = g.imgList.length - 1; i >= 0; i--) {
+ const img = g.imgList[i].childNodes[0];
+ const rect = img.getBoundingClientRect();
+ if (rect.top < -1) {
+ nextImg = img;
+ break;
+ }
+ }
+ }
+
+ if (e.code === "ArrowDown") {
+ for (let i = 0; i < g.imgList.length; i++) {
+ const img = g.imgList[i].childNodes[0];
+ const rect = img.getBoundingClientRect();
+ if (rect.top > 1) {
+ nextImg = img;
+ break;
+ }
+ }
+ }
+
+ if (nextImg !== null) {
+ e.preventDefault();
+ window.scrollTo({
+ top: nextImg.offsetTop,
+ });
+ }
+ })
+
+ await wrap(await GM.getValue("mode"));
+ } else {
+ alert(
+ "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
+ );
+ }
+ }
+ }
+
+ var switchWrap = false;
+
+ const wrap = async (width) => {
+ let img = $("#gdt").find("a");
+ let gdt = document.getElementById("gdt");
+ if (switchWrap == true) {
+ for (let i = 0; i < img.length - 1; i++) {
+ if (i % 2 !== 1) {
+ gdt.insertBefore(img[i + 1], img[i]);
+ }
+ }
+ switchWrap = false;
+ }
+
+ if ((await GM.getValue("width")) == undefined) {
+ await GM.setValue("width", "0.49");
+ console.log("set width:0.49");
+ }
+
+ if ((await GM.getValue("mode")) == undefined) {
+ await GM.setValue("mode", "double");
+ console.log("set mode:double");
+ }
+ if ((await GM.getValue("view_reverse")) == undefined) {
+ await GM.setValue("view_reverse", true);
+ console.log("set view_reverse:true");
+ }
+
+
+ img = $("#gdt").find("a");
+ let view_reverse = (await GM.getValue("view_reverse", true));
+ for (let i = 0; i < img.length; i++) {
+ let wrap = document.createElement("wrap");
+ wrap.innerHTML = "
";
+ if ((await GM.getValue("mode")) == "single") {
+ gdt.insertBefore(wrap, img[i]);
+ } else if ((await GM.getValue("mode")) == "double") {
+ if (i % 2 !== 1) {
+ gdt.insertBefore(wrap, img[i]);
+ if (view_reverse && i != img.length - 1) {
+ switchWrap = true;
+ gdt.insertBefore(img[i + 1], img[i]);
+ }
+ }
+ }
+ }
+
+ await resizeImg(await GM.getValue("width"));
+ };
+
+ const resizeImg = async (width) => {
+ const full_image = (await GM.getValue("full_image"));
+ if (full_image == true) {
+ $("#gdt")
+ .find("img")
+ .css({ "height": "100vh", "width": "auto" });
+ } else {
+ $("#gdt")
+ .find("img")
+ .css({ "height": "auto", "width": $(window).width() * width });
+ }
+ }
+
+ const adjustGmid = () => {
+ var height = $("#gd5").outerHeight(true);
+ height = height >= 330 ? height : 330;
+ $("#gmid").height(height);
+ $("#gd4").height(height);
+ };
+
+ const viewAllMode = async () => {
+ var view_all_btn = document.createElement("p");
+ var view_all = await GM.getValue("view_all", true);
+
+ view_all_btn.className = "g3";
+ view_all_btn.innerHTML = `▶ Viewer page(s): ${view_all ? "All" : "One"}`;
+ $("#gd5").append(view_all_btn);
+
+ $(".panda_view_all").on("click", async () => {
+ view_all = await GM.getValue("view_all", true);
+ GM.setValue("view_all", !view_all);
+ $(".panda_view_all").html(
+ `Viewer page(s): ${view_all ? "All" : "One"}`
+ );
+ window.location.reload(true);
+ });
+
+ adjustGmid();
+ };
+ const viewMode = async () => {
+ var view_mode = await GM.getValue("view_mode", true);
+ var view_btn = document.createElement("p");
+ view_btn.className = "g3";
+ view_btn.innerHTML = `▶ Viewer ${view_mode ? "Enabled" : "Disabled"
+ }`;
+
+ $("#gd5").append(view_btn);
+
+ $(".panda_view").on("click", async () => {
+ view_mode = await GM.getValue("view_mode", true);
+ GM.setValue("view_mode", !view_mode);
+ $(".panda_view").html(`Viewer ${!view_mode ? "Enabled" : "Disabled"}`);
+ if (view_mode) {
+ window.location.reload();
+ }
+ if (!view_mode && !viewed) {
+ viewAllMode();
+ view();
+ }
+ });
+
+ if (view_mode) {
+ viewAllMode();
+ }
+
+ adjustGmid();
+ if (view_mode) {
+ // Stop image loadding for thumbnails.
+ var imageToStop = document.querySelector("#gdt").querySelectorAll("img");
+ imageToStop.forEach((img, key) => {
+ // Only load the first thumbnail.
+ if (key == 0) {
+ return;
+ }
+ img.src = "";
+ })
+ view();
+ }
+ };
+
+ if ((e = $("img")).length === 0 && (e = $("dev")).length === 0) {
+ loginPage();
+ } else if (window.location.href.match(/^https:\/\/e[x-]hentai\.org\/g/)) {
+ downloadPage();
+ viewMode();
+ }
+});
diff --git a/web/userscripts/Simple-YouTube-Age-Restriction-Bypass.user.js b/web/userscripts/Simple-YouTube-Age-Restriction-Bypass.user.js
new file mode 100644
index 0000000..2d9401c
--- /dev/null
+++ b/web/userscripts/Simple-YouTube-Age-Restriction-Bypass.user.js
@@ -0,0 +1,1353 @@
+// ==UserScript==
+// @name Simple YouTube Age Restriction Bypass
+// @description Watch age restricted videos on YouTube without login and without age verification 😎
+// @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen 😎
+// @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge 😎
+// @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età 😎
+// @icon https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/v2.5.4/src/extension/icon/icon_64.png
+// @version 2.5.9
+// @author Zerody (https://github.com/zerodytrash)
+// @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/
+// @supportURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues
+// @license MIT
+// @match https://www.youtube.com/*
+// @match https://www.youtube-nocookie.com/*
+// @match https://m.youtube.com/*
+// @match https://music.youtube.com/*
+// @grant none
+// @run-at document-start
+// @compatible chrome
+// @compatible firefox
+// @compatible opera
+// @compatible edge
+// @compatible safari
+// ==/UserScript==
+
+/*
+ This is a transpiled version to achieve a clean code base and better browser compatibility.
+ You can find the nicely readable source code at https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
+*/
+
+(function iife(ranOnce) {
+ // Trick to get around the sandbox restrictions in Greasemonkey (Firefox)
+ // Inject code into the main window if criteria match
+ if (this !== window && !ranOnce) {
+ window.eval('(' + iife.toString() + ')(true);');
+ return;
+ }
+
+ // Script configuration variables
+ const UNLOCKABLE_PLAYABILITY_STATUSES = ['AGE_VERIFICATION_REQUIRED', 'AGE_CHECK_REQUIRED', 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
+ const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];
+
+ // These are the proxy servers that are sometimes required to unlock videos with age restrictions.
+ // You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
+ // To learn what information is transferred, please read: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#privacy
+ const ACCOUNT_PROXY_SERVER_HOST = 'https://youtube-proxy.zerody.one';
+ const VIDEO_PROXY_SERVER_HOST = 'https://ny.4everproxy.com';
+
+ // User needs to confirm the unlock process on embedded player?
+ let ENABLE_UNLOCK_CONFIRMATION_EMBED = true;
+
+ // Show notification?
+ let ENABLE_UNLOCK_NOTIFICATION = true;
+
+ // Disable content warnings?
+ let SKIP_CONTENT_WARNINGS = true;
+
+ // Some Innertube bypass methods require the following authentication headers of the currently logged in user.
+ const GOOGLE_AUTH_HEADER_NAMES = ['Authorization', 'X-Goog-AuthUser', 'X-Origin'];
+
+ /**
+ * The SQP parameter length is different for blurred thumbnails.
+ * They contain much less information, than normal thumbnails.
+ * The thumbnail SQPs tend to have a long and a short version.
+ */
+ const BLURRED_THUMBNAIL_SQP_LENGTHS = [
+ 32, // Mobile (SHORT)
+ 48, // Desktop Playlist (SHORT)
+ 56, // Desktop (SHORT)
+ 68, // Mobile (LONG)
+ 72, // Mobile Shorts
+ 84, // Desktop Playlist (LONG)
+ 88, // Desktop (LONG)
+ ];
+
+ // small hack to prevent tree shaking on these exports
+ var Config = window[Symbol()] = {
+ UNLOCKABLE_PLAYABILITY_STATUSES,
+ VALID_PLAYABILITY_STATUSES,
+ ACCOUNT_PROXY_SERVER_HOST,
+ VIDEO_PROXY_SERVER_HOST,
+ ENABLE_UNLOCK_CONFIRMATION_EMBED,
+ ENABLE_UNLOCK_NOTIFICATION,
+ SKIP_CONTENT_WARNINGS,
+ GOOGLE_AUTH_HEADER_NAMES,
+ BLURRED_THUMBNAIL_SQP_LENGTHS,
+ };
+
+ function isGoogleVideoUrl(url) {
+ return url.host.includes('.googlevideo.com');
+ }
+
+ function isGoogleVideoUnlockRequired(googleVideoUrl, lastProxiedGoogleVideoId) {
+ const urlParams = new URLSearchParams(googleVideoUrl.search);
+ const hasGcrFlag = urlParams.get('gcr');
+ const wasUnlockedByAccountProxy = urlParams.get('id') === lastProxiedGoogleVideoId;
+
+ return hasGcrFlag && wasUnlockedByAccountProxy;
+ }
+
+ const nativeJSONParse = window.JSON.parse;
+ const nativeXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;
+
+ const isDesktop = window.location.host !== 'm.youtube.com';
+ const isMusic = window.location.host === 'music.youtube.com';
+ const isEmbed = window.location.pathname.indexOf('/embed/') === 0;
+ const isConfirmed = window.location.search.includes('unlock_confirmed');
+
+ class Deferred {
+ constructor() {
+ return Object.assign(
+ new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ }),
+ this,
+ );
+ }
+ }
+
+ function createElement(tagName, options) {
+ const node = document.createElement(tagName);
+ options && Object.assign(node, options);
+ return node;
+ }
+
+ function isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+ function findNestedObjectsByAttributeNames(object, attributeNames) {
+ var results = [];
+
+ // Does the current object match the attribute conditions?
+ if (attributeNames.every((key) => typeof object[key] !== 'undefined')) {
+ results.push(object);
+ }
+
+ // Diggin' deeper for each nested object (recursive)
+ Object.keys(object).forEach((key) => {
+ if (object[key] && typeof object[key] === 'object') {
+ results.push(...findNestedObjectsByAttributeNames(object[key], attributeNames));
+ }
+ });
+
+ return results;
+ }
+
+ function pageLoaded() {
+ if (document.readyState === 'complete') return Promise.resolve();
+
+ const deferred = new Deferred();
+
+ window.addEventListener('load', deferred.resolve, { once: true });
+
+ return deferred;
+ }
+
+ function createDeepCopy(obj) {
+ return nativeJSONParse(JSON.stringify(obj));
+ }
+
+ function getYtcfgValue(name) {
+ var _window$ytcfg;
+ return (_window$ytcfg = window.ytcfg) === null || _window$ytcfg === void 0 ? void 0 : _window$ytcfg.get(name);
+ }
+
+ function getSignatureTimestamp() {
+ return (
+ getYtcfgValue('STS')
+ || (() => {
+ var _document$querySelect;
+ // STS is missing on embedded player. Retrieve from player base script as fallback...
+ const playerBaseJsPath = (_document$querySelect = document.querySelector('script[src*="/base.js"]')) === null || _document$querySelect === void 0
+ ? void 0
+ : _document$querySelect.src;
+
+ if (!playerBaseJsPath) return;
+
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('GET', playerBaseJsPath, false);
+ xmlhttp.send(null);
+
+ return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
+ })()
+ );
+ }
+
+ function isUserLoggedIn() {
+ // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
+ if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
+ if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
+ if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;
+
+ return false;
+ }
+
+ function getCurrentVideoStartTime(currentVideoId) {
+ // Check if the URL corresponds to the requested video
+ // This is not the case when the player gets preloaded for the next video in a playlist.
+ if (window.location.href.includes(currentVideoId)) {
+ var _ref;
+ // "t"-param on youtu.be urls
+ // "start"-param on embed player
+ // "time_continue" when clicking "watch on youtube" on embedded player
+ const urlParams = new URLSearchParams(window.location.search);
+ const startTimeString = (_ref = urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue')) === null || _ref === void 0
+ ? void 0
+ : _ref.replace('s', '');
+
+ if (startTimeString && !isNaN(startTimeString)) {
+ return parseInt(startTimeString);
+ }
+ }
+
+ return 0;
+ }
+
+ function setUrlParams(params) {
+ const urlParams = new URLSearchParams(window.location.search);
+ for (const paramName in params) {
+ urlParams.set(paramName, params[paramName]);
+ }
+ window.location.search = urlParams;
+ }
+
+ function waitForElement(elementSelector, timeout) {
+ const deferred = new Deferred();
+
+ const checkDomInterval = setInterval(() => {
+ const elem = document.querySelector(elementSelector);
+ if (elem) {
+ clearInterval(checkDomInterval);
+ deferred.resolve(elem);
+ }
+ }, 100);
+
+ if (timeout) {
+ setTimeout(() => {
+ clearInterval(checkDomInterval);
+ deferred.reject();
+ }, timeout);
+ }
+
+ return deferred;
+ }
+
+ function parseRelativeUrl(url) {
+ if (typeof url !== 'string') {
+ return null;
+ }
+
+ if (url.indexOf('/') === 0) {
+ url = window.location.origin + url;
+ }
+
+ try {
+ return url.indexOf('https://') === 0 ? new window.URL(url) : null;
+ } catch {
+ return null;
+ }
+ }
+
+ function isWatchNextObject(parsedData) {
+ var _parsedData$currentVi;
+ if (
+ !(parsedData !== null && parsedData !== void 0 && parsedData.contents)
+ || !(parsedData !== null && parsedData !== void 0 && (_parsedData$currentVi = parsedData.currentVideoEndpoint) !== null && _parsedData$currentVi !== void 0
+ && (_parsedData$currentVi = _parsedData$currentVi.watchEndpoint) !== null && _parsedData$currentVi !== void 0 && _parsedData$currentVi.videoId)
+ ) return false;
+ return !!parsedData.contents.twoColumnWatchNextResults || !!parsedData.contents.singleColumnWatchNextResults;
+ }
+
+ function isWatchNextSidebarEmpty(parsedData) {
+ var _parsedData$contents2, _content$find;
+ if (isDesktop) {
+ var _parsedData$contents;
+ // WEB response layout
+ const result = (_parsedData$contents = parsedData.contents) === null || _parsedData$contents === void 0
+ || (_parsedData$contents = _parsedData$contents.twoColumnWatchNextResults) === null || _parsedData$contents === void 0
+ || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
+ || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
+ ? void 0
+ : _parsedData$contents.results;
+ return !result;
+ }
+
+ // MWEB response layout
+ const content = (_parsedData$contents2 = parsedData.contents) === null || _parsedData$contents2 === void 0
+ || (_parsedData$contents2 = _parsedData$contents2.singleColumnWatchNextResults) === null || _parsedData$contents2 === void 0
+ || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
+ || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
+ ? void 0
+ : _parsedData$contents2.contents;
+ const result = content === null || content === void 0 || (_content$find = content.find((e) => {
+ var _e$itemSectionRendere;
+ return ((_e$itemSectionRendere = e.itemSectionRenderer) === null || _e$itemSectionRendere === void 0 ? void 0 : _e$itemSectionRendere.targetId)
+ === 'watch-next-feed';
+ })) === null
+ || _content$find === void 0
+ ? void 0
+ : _content$find.itemSectionRenderer;
+ return typeof result !== 'object';
+ }
+
+ function isPlayerObject(parsedData) {
+ return (parsedData === null || parsedData === void 0 ? void 0 : parsedData.videoDetails)
+ && (parsedData === null || parsedData === void 0 ? void 0 : parsedData.playabilityStatus);
+ }
+
+ function isEmbeddedPlayerObject(parsedData) {
+ return typeof (parsedData === null || parsedData === void 0 ? void 0 : parsedData.previewPlayabilityStatus) === 'object';
+ }
+
+ function isAgeRestricted(playabilityStatus) {
+ var _playabilityStatus$er;
+ if (!(playabilityStatus !== null && playabilityStatus !== void 0 && playabilityStatus.status)) return false;
+ if (playabilityStatus.desktopLegacyAgeGateReason) return true;
+ if (Config.UNLOCKABLE_PLAYABILITY_STATUSES.includes(playabilityStatus.status)) return true;
+
+ // Fix to detect age restrictions on embed player
+ // see https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/85#issuecomment-946853553
+ return (
+ isEmbed
+ && ((_playabilityStatus$er = playabilityStatus.errorScreen) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.playerErrorMessageRenderer) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.reason) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.runs) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.find((x) => x.navigationEndpoint)) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.navigationEndpoint) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.urlEndpoint) === null || _playabilityStatus$er === void 0
+ || (_playabilityStatus$er = _playabilityStatus$er.url) === null || _playabilityStatus$er === void 0
+ ? void 0
+ : _playabilityStatus$er.includes('/2802167'))
+ );
+ }
+
+ function isSearchResult(parsedData) {
+ var _parsedData$contents3, _parsedData$contents4, _parsedData$onRespons;
+ return (
+ typeof (parsedData === null || parsedData === void 0 || (_parsedData$contents3 = parsedData.contents) === null || _parsedData$contents3 === void 0
+ ? void 0
+ : _parsedData$contents3.twoColumnSearchResultsRenderer) === 'object' // Desktop initial results
+ || (parsedData === null || parsedData === void 0 || (_parsedData$contents4 = parsedData.contents) === null || _parsedData$contents4 === void 0
+ || (_parsedData$contents4 = _parsedData$contents4.sectionListRenderer) === null || _parsedData$contents4 === void 0
+ ? void 0
+ : _parsedData$contents4.targetId) === 'search-feed' // Mobile initial results
+ || (parsedData === null || parsedData === void 0 || (_parsedData$onRespons = parsedData.onResponseReceivedCommands) === null || _parsedData$onRespons === void 0
+ || (_parsedData$onRespons = _parsedData$onRespons.find((x) => x.appendContinuationItemsAction)) === null || _parsedData$onRespons === void 0
+ || (_parsedData$onRespons = _parsedData$onRespons.appendContinuationItemsAction) === null || _parsedData$onRespons === void 0
+ ? void 0
+ : _parsedData$onRespons.targetId) === 'search-feed' // Desktop & Mobile scroll continuation
+ );
+ }
+
+ function attach$4(obj, prop, onCall) {
+ if (!obj || typeof obj[prop] !== 'function') {
+ return;
+ }
+
+ let original = obj[prop];
+
+ obj[prop] = function() {
+ try {
+ onCall(arguments);
+ } catch {}
+ original.apply(this, arguments);
+ };
+ }
+
+ const logPrefix = '%cSimple-YouTube-Age-Restriction-Bypass:';
+ const logPrefixStyle = 'background-color: #1e5c85; color: #fff; font-size: 1.2em;';
+ const logSuffix = '\uD83D\uDC1E You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues';
+
+ function error(err, msg) {
+ console.error(logPrefix, logPrefixStyle, msg, err, getYtcfgDebugString(), '\n\n', logSuffix);
+ if (window.SYARB_CONFIG) {
+ window.dispatchEvent(
+ new CustomEvent('SYARB_LOG_ERROR', {
+ detail: {
+ message: (msg ? msg + '; ' : '') + (err && err.message ? err.message : ''),
+ stack: err && err.stack ? err.stack : null,
+ },
+ }),
+ );
+ }
+ }
+
+ function info(msg) {
+ console.info(logPrefix, logPrefixStyle, msg);
+ if (window.SYARB_CONFIG) {
+ window.dispatchEvent(
+ new CustomEvent('SYARB_LOG_INFO', {
+ detail: {
+ message: msg,
+ },
+ }),
+ );
+ }
+ }
+
+ function getYtcfgDebugString() {
+ try {
+ return (
+ `InnertubeConfig: `
+ + `innertubeApiKey: ${getYtcfgValue('INNERTUBE_API_KEY')} `
+ + `innertubeClientName: ${getYtcfgValue('INNERTUBE_CLIENT_NAME')} `
+ + `innertubeClientVersion: ${getYtcfgValue('INNERTUBE_CLIENT_VERSION')} `
+ + `loggedIn: ${getYtcfgValue('LOGGED_IN')} `
+ );
+ } catch (err) {
+ return `Failed to access config: ${err}`;
+ }
+ }
+
+ /**
+ * And here we deal with YouTube's crappy initial data (present in page source) and the problems that occur when intercepting that data.
+ * YouTube has some protections in place that make it difficult to intercept and modify the global ytInitialPlayerResponse variable.
+ * The easiest way would be to set a descriptor on that variable to change the value directly on declaration.
+ * But some adblockers define their own descriptors on the ytInitialPlayerResponse variable, which makes it hard to register another descriptor on it.
+ * As a workaround only the relevant playerResponse property of the ytInitialPlayerResponse variable will be intercepted.
+ * This is achieved by defining a descriptor on the object prototype for that property, which affects any object with a `playerResponse` property.
+ */
+ function attach$3(onInitialData) {
+ interceptObjectProperty('playerResponse', (obj, playerResponse) => {
+ info(`playerResponse property set, contains sidebar: ${!!obj.response}`);
+
+ // The same object also contains the sidebar data and video description
+ if (isObject(obj.response)) onInitialData(obj.response);
+
+ // If the script is executed too late and the bootstrap data has already been processed,
+ // a reload of the player can be forced by creating a deep copy of the object.
+ // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
+ playerResponse.unlocked = false;
+ onInitialData(playerResponse);
+ return playerResponse.unlocked ? createDeepCopy(playerResponse) : playerResponse;
+ });
+
+ // The global `ytInitialData` variable can be modified on the fly.
+ // It contains search results, sidebar data and meta information
+ // Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
+ window.addEventListener('DOMContentLoaded', () => {
+ if (isObject(window.ytInitialData)) {
+ onInitialData(window.ytInitialData);
+ }
+ });
+ }
+
+ function interceptObjectProperty(prop, onSet) {
+ var _Object$getOwnPropert;
+ // Allow other userscripts to decorate this descriptor, if they do something similar
+ const dataKey = '__SYARB_' + prop;
+ const { get: getter, set: setter } = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(Object.prototype, prop)) !== null && _Object$getOwnPropert !== void 0
+ ? _Object$getOwnPropert
+ : {
+ set(value) {
+ this[dataKey] = value;
+ },
+ get() {
+ return this[dataKey];
+ },
+ };
+
+ // Intercept the given property on any object
+ // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
+ Object.defineProperty(Object.prototype, prop, {
+ set(value) {
+ setter.call(this, isObject(value) ? onSet(this, value) : value);
+ },
+ get() {
+ return getter.call(this);
+ },
+ configurable: true,
+ });
+ }
+
+ // Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
+ function attach$2(onJsonDataReceived) {
+ window.JSON.parse = function() {
+ const data = nativeJSONParse.apply(this, arguments);
+ return isObject(data) ? onJsonDataReceived(data) : data;
+ };
+ }
+
+ function attach$1(onRequestCreate) {
+ if (typeof window.Request !== 'function') {
+ return;
+ }
+
+ window.Request = new Proxy(window.Request, {
+ construct(target, args) {
+ const [url, options] = args;
+ try {
+ const parsedUrl = parseRelativeUrl(url);
+ const modifiedUrl = onRequestCreate(parsedUrl, options);
+
+ if (modifiedUrl) {
+ args[0] = modifiedUrl.toString();
+ }
+ } catch (err) {
+ error(err, `Failed to intercept Request()`);
+ }
+
+ return Reflect.construct(...arguments);
+ },
+ });
+ }
+
+ function attach(onXhrOpenCalled) {
+ XMLHttpRequest.prototype.open = function(method, url) {
+ try {
+ let parsedUrl = parseRelativeUrl(url);
+
+ if (parsedUrl) {
+ const modifiedUrl = onXhrOpenCalled(method, parsedUrl, this);
+
+ if (modifiedUrl) {
+ arguments[1] = modifiedUrl.toString();
+ }
+ }
+ } catch (err) {
+ error(err, `Failed to intercept XMLHttpRequest.open()`);
+ }
+
+ nativeXMLHttpRequestOpen.apply(this, arguments);
+ };
+ }
+
+ const localStoragePrefix = 'SYARB_';
+
+ function set(key, value) {
+ localStorage.setItem(localStoragePrefix + key, JSON.stringify(value));
+ }
+
+ function get(key) {
+ try {
+ return JSON.parse(localStorage.getItem(localStoragePrefix + key));
+ } catch {
+ return null;
+ }
+ }
+
+ function getPlayer$1(payload, useAuth) {
+ return sendInnertubeRequest('v1/player', payload, useAuth);
+ }
+
+ function getNext$1(payload, useAuth) {
+ return sendInnertubeRequest('v1/next', payload, useAuth);
+ }
+
+ function sendInnertubeRequest(endpoint, payload, useAuth) {
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);
+
+ if (useAuth && isUserLoggedIn()) {
+ xmlhttp.withCredentials = true;
+ Config.GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
+ xmlhttp.setRequestHeader(headerName, get(headerName));
+ });
+ }
+
+ xmlhttp.send(JSON.stringify(payload));
+ return nativeJSONParse(xmlhttp.responseText);
+ }
+
+ var innertube = {
+ getPlayer: getPlayer$1,
+ getNext: getNext$1,
+ };
+
+ let nextResponseCache = {};
+
+ function getGoogleVideoUrl(originalUrl) {
+ return Config.VIDEO_PROXY_SERVER_HOST + '/direct/' + btoa(originalUrl.toString());
+ }
+
+ function getPlayer(payload) {
+ // Also request the /next response if a later /next request is likely.
+ if (!nextResponseCache[payload.videoId] && !isMusic && !isEmbed) {
+ payload.includeNext = 1;
+ }
+
+ return sendRequest('getPlayer', payload);
+ }
+
+ function getNext(payload) {
+ // Next response already cached? => Return cached content
+ if (nextResponseCache[payload.videoId]) {
+ return nextResponseCache[payload.videoId];
+ }
+
+ return sendRequest('getNext', payload);
+ }
+
+ function sendRequest(endpoint, payload) {
+ const queryParams = new URLSearchParams(payload);
+ const proxyUrl = `${Config.ACCOUNT_PROXY_SERVER_HOST}/${endpoint}?${queryParams}&client=js`;
+
+ try {
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('GET', proxyUrl, false);
+ xmlhttp.send(null);
+
+ const proxyResponse = nativeJSONParse(xmlhttp.responseText);
+
+ // Mark request as 'proxied'
+ proxyResponse.proxied = true;
+
+ // Put included /next response in the cache
+ if (proxyResponse.nextResponse) {
+ nextResponseCache[payload.videoId] = proxyResponse.nextResponse;
+ delete proxyResponse.nextResponse;
+ }
+
+ return proxyResponse;
+ } catch (err) {
+ error(err, 'Proxy API Error');
+ return { errorMessage: 'Proxy Connection failed' };
+ }
+ }
+
+ var proxy = {
+ getPlayer,
+ getNext,
+ getGoogleVideoUrl,
+ };
+
+ function getUnlockStrategies$1(videoId, lastPlayerUnlockReason) {
+ var _getYtcfgValue$client;
+ const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
+ const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
+ const hl = getYtcfgValue('HL');
+ const userInterfaceTheme = (_getYtcfgValue$client = getYtcfgValue('INNERTUBE_CONTEXT').client.userInterfaceTheme) !== null && _getYtcfgValue$client !== void 0
+ ? _getYtcfgValue$client
+ : document.documentElement.hasAttribute('dark')
+ ? 'USER_INTERFACE_THEME_DARK'
+ : 'USER_INTERFACE_THEME_LIGHT';
+
+ return [
+ /**
+ * Retrieve the sidebar and video description by just adding `racyCheckOk` and `contentCheckOk` params
+ * This strategy can be used to bypass content warnings
+ */
+ {
+ name: 'Content Warning Bypass',
+ skip: !lastPlayerUnlockReason || !lastPlayerUnlockReason.includes('CHECK_REQUIRED'),
+ optionalAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName,
+ clientVersion,
+ hl,
+ userInterfaceTheme,
+ },
+ },
+ videoId,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the sidebar and video description from an account proxy server.
+ * Session cookies of an age-verified Google account are stored on server side.
+ * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
+ */
+ {
+ name: 'Account Proxy',
+ payload: {
+ videoId,
+ clientName,
+ clientVersion,
+ hl,
+ userInterfaceTheme,
+ isEmbed: +isEmbed,
+ isConfirmed: +isConfirmed,
+ },
+ endpoint: proxy,
+ },
+ ];
+ }
+
+ function getUnlockStrategies(videoId, reason) {
+ const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
+ const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
+ const signatureTimestamp = getSignatureTimestamp();
+ const startTimeSecs = getCurrentVideoStartTime(videoId);
+ const hl = getYtcfgValue('HL');
+
+ return [
+ /**
+ * Retrieve the video info by just adding `racyCheckOk` and `contentCheckOk` params
+ * This strategy can be used to bypass content warnings
+ */
+ {
+ name: 'Content Warning Bypass',
+ skip: !reason || !reason.includes('CHECK_REQUIRED'),
+ optionalAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName: clientName,
+ clientVersion: clientVersion,
+ hl,
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the video info by using the TVHTML5 Embedded client
+ * This client has no age restrictions in place (2022-03-28)
+ * See https://github.com/zerodytrash/YouTube-Internal-Clients
+ */
+ {
+ name: 'TV Embedded Player',
+ requiresAuth: false,
+ payload: {
+ context: {
+ client: {
+ clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
+ clientVersion: '2.0',
+ clientScreen: 'WATCH',
+ hl,
+ },
+ thirdParty: {
+ embedUrl: 'https://www.youtube.com/',
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the video info by using the WEB_CREATOR client in combination with user authentication
+ * Requires that the user is logged in. Can bypass the tightened age verification in the EU.
+ * See https://github.com/yt-dlp/yt-dlp/pull/600
+ */
+ {
+ name: 'Creator + Auth',
+ requiresAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName: 'WEB_CREATOR',
+ clientVersion: '1.20210909.07.00',
+ hl,
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the video info from an account proxy server.
+ * Session cookies of an age-verified Google account are stored on server side.
+ * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
+ */
+ {
+ name: 'Account Proxy',
+ payload: {
+ videoId,
+ reason,
+ clientName,
+ clientVersion,
+ signatureTimestamp,
+ startTimeSecs,
+ hl,
+ isEmbed: +isEmbed,
+ isConfirmed: +isConfirmed,
+ },
+ endpoint: proxy,
+ },
+ ];
+ }
+
+ var buttonTemplate =
+ '';
+
+ let buttons = {};
+
+ async function addButton(id, text, backgroundColor, onClick) {
+ const errorScreenElement = await waitForElement('.ytp-error', 2000);
+ const buttonElement = createElement('div', { class: 'button-container', innerHTML: buttonTemplate });
+ buttonElement.getElementsByClassName('button-text')[0].innerText = text;
+
+ if (backgroundColor) {
+ buttonElement.querySelector(':scope > div').style['background-color'] = backgroundColor;
+ }
+
+ if (typeof onClick === 'function') {
+ buttonElement.addEventListener('click', onClick);
+ }
+
+ // Button already attached?
+ if (buttons[id] && buttons[id].isConnected) {
+ return;
+ }
+
+ buttons[id] = buttonElement;
+ errorScreenElement.append(buttonElement);
+ }
+
+ function removeButton(id) {
+ if (buttons[id] && buttons[id].isConnected) {
+ buttons[id].remove();
+ }
+ }
+
+ const confirmationButtonId = 'confirmButton';
+ const confirmationButtonText = 'Click to unlock';
+
+ function isConfirmationRequired() {
+ return !isConfirmed && isEmbed && Config.ENABLE_UNLOCK_CONFIRMATION_EMBED;
+ }
+
+ function requestConfirmation() {
+ addButton(confirmationButtonId, confirmationButtonText, null, () => {
+ removeButton(confirmationButtonId);
+ confirm();
+ });
+ }
+
+ function confirm() {
+ setUrlParams({
+ unlock_confirmed: 1,
+ autoplay: 1,
+ });
+ }
+
+ var tDesktop = '\n';
+
+ var tMobile =
+ '\n \n \n \n\n';
+
+ const template = isDesktop ? tDesktop : tMobile;
+
+ const nToastContainer = createElement('div', { id: 'toast-container', innerHTML: template });
+ const nToast = nToastContainer.querySelector(':scope > *');
+
+ // On YT Music show the toast above the player controls
+ if (isMusic) {
+ nToast.style['margin-bottom'] = '85px';
+ }
+
+ if (!isDesktop) {
+ nToast.nMessage = nToast.querySelector('.notification-action-response-text');
+ nToast.show = (message) => {
+ nToast.nMessage.innerText = message;
+ nToast.setAttribute('dir', 'in');
+ setTimeout(() => {
+ nToast.setAttribute('dir', 'out');
+ }, nToast.duration + 225);
+ };
+ }
+
+ async function show(message, duration = 5) {
+ if (!Config.ENABLE_UNLOCK_NOTIFICATION) return;
+ if (isEmbed) return;
+
+ await pageLoaded();
+
+ // Do not show notification when tab is in background
+ if (document.visibilityState === 'hidden') return;
+
+ // Append toast container to DOM, if not already done
+ if (!nToastContainer.isConnected) document.documentElement.append(nToastContainer);
+
+ nToast.duration = duration * 1000;
+ nToast.show(message);
+ }
+
+ var Toast = { show };
+
+ const messagesMap = {
+ success: 'Age-restricted video successfully unlocked!',
+ fail: 'Unable to unlock this video 🙁 - More information in the developer console',
+ };
+
+ let lastPlayerUnlockVideoId = null;
+ let lastPlayerUnlockReason = null;
+
+ let lastProxiedGoogleVideoUrlParams;
+ let cachedPlayerResponse = {};
+
+ function getLastProxiedGoogleVideoId() {
+ var _lastProxiedGoogleVid;
+ return (_lastProxiedGoogleVid = lastProxiedGoogleVideoUrlParams) === null || _lastProxiedGoogleVid === void 0 ? void 0 : _lastProxiedGoogleVid.get('id');
+ }
+
+ function unlockResponse$1(playerResponse) {
+ var _playerResponse$video, _playerResponse$playa, _playerResponse$previ, _unlockedPlayerRespon, _unlockedPlayerRespon3;
+ // Check if the user has to confirm the unlock first
+ if (isConfirmationRequired()) {
+ info('Unlock confirmation required.');
+ requestConfirmation();
+ return;
+ }
+
+ const videoId = ((_playerResponse$video = playerResponse.videoDetails) === null || _playerResponse$video === void 0 ? void 0 : _playerResponse$video.videoId)
+ || getYtcfgValue('PLAYER_VARS').video_id;
+ const reason = ((_playerResponse$playa = playerResponse.playabilityStatus) === null || _playerResponse$playa === void 0 ? void 0 : _playerResponse$playa.status)
+ || ((_playerResponse$previ = playerResponse.previewPlayabilityStatus) === null || _playerResponse$previ === void 0 ? void 0 : _playerResponse$previ.status);
+
+ if (!Config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
+ info(`SKIP_CONTENT_WARNINGS disabled and ${reason} status detected.`);
+ return;
+ }
+
+ lastPlayerUnlockVideoId = videoId;
+ lastPlayerUnlockReason = reason;
+
+ const unlockedPlayerResponse = getUnlockedPlayerResponse(videoId, reason);
+
+ // account proxy error?
+ if (unlockedPlayerResponse.errorMessage) {
+ Toast.show(`${messagesMap.fail} (ProxyError)`, 10);
+ throw new Error(`Player Unlock Failed, Proxy Error Message: ${unlockedPlayerResponse.errorMessage}`);
+ }
+
+ // check if the unlocked response isn't playable
+ if (
+ !Config.VALID_PLAYABILITY_STATUSES.includes(
+ (_unlockedPlayerRespon = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon === void 0 ? void 0 : _unlockedPlayerRespon.status,
+ )
+ ) {
+ var _unlockedPlayerRespon2;
+ Toast.show(`${messagesMap.fail} (PlayabilityError)`, 10);
+ throw new Error(
+ `Player Unlock Failed, playabilityStatus: ${
+ (_unlockedPlayerRespon2 = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon2 === void 0 ? void 0 : _unlockedPlayerRespon2.status
+ }`,
+ );
+ }
+
+ // if the video info was retrieved via proxy, store the URL params from the url-attribute to detect later if the requested video file (googlevideo.com) need a proxy.
+ if (
+ unlockedPlayerResponse.proxied && (_unlockedPlayerRespon3 = unlockedPlayerResponse.streamingData) !== null && _unlockedPlayerRespon3 !== void 0
+ && _unlockedPlayerRespon3.adaptiveFormats
+ ) {
+ var _unlockedPlayerRespon4, _unlockedPlayerRespon5;
+ const cipherText = (_unlockedPlayerRespon4 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) =>
+ x.signatureCipher
+ )) === null || _unlockedPlayerRespon4 === void 0
+ ? void 0
+ : _unlockedPlayerRespon4.signatureCipher;
+ const videoUrl = cipherText
+ ? new URLSearchParams(cipherText).get('url')
+ : (_unlockedPlayerRespon5 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) => x.url)) === null || _unlockedPlayerRespon5 === void 0
+ ? void 0
+ : _unlockedPlayerRespon5.url;
+
+ lastProxiedGoogleVideoUrlParams = videoUrl ? new URLSearchParams(new window.URL(videoUrl).search) : null;
+ }
+
+ // Overwrite the embedded (preview) playabilityStatus with the unlocked one
+ if (playerResponse.previewPlayabilityStatus) {
+ playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;
+ }
+
+ // Transfer all unlocked properties to the original player response
+ Object.assign(playerResponse, unlockedPlayerResponse);
+
+ playerResponse.unlocked = true;
+
+ Toast.show(messagesMap.success);
+ }
+
+ function getUnlockedPlayerResponse(videoId, reason) {
+ // Check if response is cached
+ if (cachedPlayerResponse.videoId === videoId) return createDeepCopy(cachedPlayerResponse);
+
+ const unlockStrategies = getUnlockStrategies(videoId, reason);
+
+ let unlockedPlayerResponse = {};
+
+ // Try every strategy until one of them works
+ unlockStrategies.every((strategy, index) => {
+ var _unlockedPlayerRespon6;
+ // Skip strategy if authentication is required and the user is not logged in
+ if (strategy.skip || strategy.requiresAuth && !isUserLoggedIn()) return true;
+
+ info(`Trying Player Unlock Method #${index + 1} (${strategy.name})`);
+
+ try {
+ unlockedPlayerResponse = strategy.endpoint.getPlayer(strategy.payload, strategy.requiresAuth || strategy.optionalAuth);
+ } catch (err) {
+ error(err, `Player Unlock Method ${index + 1} failed with exception`);
+ }
+
+ const isStatusValid = Config.VALID_PLAYABILITY_STATUSES.includes(
+ (_unlockedPlayerRespon6 = unlockedPlayerResponse) === null || _unlockedPlayerRespon6 === void 0
+ || (_unlockedPlayerRespon6 = _unlockedPlayerRespon6.playabilityStatus) === null || _unlockedPlayerRespon6 === void 0
+ ? void 0
+ : _unlockedPlayerRespon6.status,
+ );
+
+ if (isStatusValid) {
+ var _unlockedPlayerRespon7;
+ /**
+ * Workaround: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/191
+ *
+ * YouTube checks if the `trackingParams` in the response matches the decoded `trackingParam` in `responseContext.mainAppWebResponseContext`.
+ * However, sometimes the response does not include the `trackingParam` in the `responseContext`, causing the check to fail.
+ *
+ * This workaround addresses the issue by hardcoding the `trackingParams` in the response context.
+ */
+ if (
+ !unlockedPlayerResponse.trackingParams
+ || !((_unlockedPlayerRespon7 = unlockedPlayerResponse.responseContext) !== null && _unlockedPlayerRespon7 !== void 0
+ && (_unlockedPlayerRespon7 = _unlockedPlayerRespon7.mainAppWebResponseContext) !== null && _unlockedPlayerRespon7 !== void 0
+ && _unlockedPlayerRespon7.trackingParam)
+ ) {
+ unlockedPlayerResponse.trackingParams = 'CAAQu2kiEwjor8uHyOL_AhWOvd4KHavXCKw=';
+ unlockedPlayerResponse.responseContext = {
+ mainAppWebResponseContext: {
+ trackingParam: 'kx_fmPxhoPZRzgL8kzOwANUdQh8ZwHTREkw2UqmBAwpBYrzRgkuMsNLBwOcCE59TDtslLKPQ-SS',
+ },
+ };
+ }
+
+ /**
+ * Workaround: Account proxy response currently does not include `playerConfig`
+ *
+ * Stays here until we rewrite the account proxy to only include the necessary and bare minimum response
+ */
+ if (strategy.payload.startTimeSecs && strategy.name === 'Account Proxy') {
+ unlockedPlayerResponse.playerConfig = {
+ playbackStartConfig: {
+ startSeconds: strategy.payload.startTimeSecs,
+ },
+ };
+ }
+ }
+
+ return !isStatusValid;
+ });
+
+ // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
+ cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };
+
+ return unlockedPlayerResponse;
+ }
+
+ let cachedNextResponse = {};
+
+ function unlockResponse(originalNextResponse) {
+ const videoId = originalNextResponse.currentVideoEndpoint.watchEndpoint.videoId;
+
+ if (!videoId) {
+ throw new Error(`Missing videoId in nextResponse`);
+ }
+
+ // Only unlock the /next response when the player has been unlocked as well
+ if (videoId !== lastPlayerUnlockVideoId) {
+ return;
+ }
+
+ const unlockedNextResponse = getUnlockedNextResponse(videoId);
+
+ // check if the sidebar of the unlocked response is still empty
+ if (isWatchNextSidebarEmpty(unlockedNextResponse)) {
+ throw new Error(`Sidebar Unlock Failed`);
+ }
+
+ // Transfer some parts of the unlocked response to the original response
+ mergeNextResponse(originalNextResponse, unlockedNextResponse);
+ }
+
+ function getUnlockedNextResponse(videoId) {
+ // Check if response is cached
+ if (cachedNextResponse.videoId === videoId) return createDeepCopy(cachedNextResponse);
+
+ const unlockStrategies = getUnlockStrategies$1(videoId, lastPlayerUnlockReason);
+
+ let unlockedNextResponse = {};
+
+ // Try every strategy until one of them works
+ unlockStrategies.every((strategy, index) => {
+ if (strategy.skip) return true;
+
+ info(`Trying Next Unlock Method #${index + 1} (${strategy.name})`);
+
+ try {
+ unlockedNextResponse = strategy.endpoint.getNext(strategy.payload, strategy.optionalAuth);
+ } catch (err) {
+ error(err, `Next Unlock Method ${index + 1} failed with exception`);
+ }
+
+ return isWatchNextSidebarEmpty(unlockedNextResponse);
+ });
+
+ // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
+ cachedNextResponse = { videoId, ...createDeepCopy(unlockedNextResponse) };
+
+ return unlockedNextResponse;
+ }
+
+ function mergeNextResponse(originalNextResponse, unlockedNextResponse) {
+ var _unlockedNextResponse;
+ if (isDesktop) {
+ // Transfer WatchNextResults to original response
+ originalNextResponse.contents.twoColumnWatchNextResults.secondaryResults = unlockedNextResponse.contents.twoColumnWatchNextResults.secondaryResults;
+
+ // Transfer video description to original response
+ const originalVideoSecondaryInfoRenderer = originalNextResponse.contents.twoColumnWatchNextResults.results.results.contents.find(
+ (x) => x.videoSecondaryInfoRenderer,
+ ).videoSecondaryInfoRenderer;
+ const unlockedVideoSecondaryInfoRenderer = unlockedNextResponse.contents.twoColumnWatchNextResults.results.results.contents.find(
+ (x) => x.videoSecondaryInfoRenderer,
+ ).videoSecondaryInfoRenderer;
+
+ // TODO: Throw if description not found?
+ if (unlockedVideoSecondaryInfoRenderer.description) {
+ originalVideoSecondaryInfoRenderer.description = unlockedVideoSecondaryInfoRenderer.description;
+ } else if (unlockedVideoSecondaryInfoRenderer.attributedDescription) {
+ originalVideoSecondaryInfoRenderer.attributedDescription = unlockedVideoSecondaryInfoRenderer.attributedDescription;
+ }
+
+ return;
+ }
+
+ // Transfer WatchNextResults to original response
+ const unlockedWatchNextFeed = (_unlockedNextResponse = unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
+ || (_unlockedNextResponse = _unlockedNextResponse.singleColumnWatchNextResults) === null || _unlockedNextResponse === void 0
+ || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
+ || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
+ || (_unlockedNextResponse = _unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
+ ? void 0
+ : _unlockedNextResponse.find(
+ (x) => {
+ var _x$itemSectionRendere;
+ return ((_x$itemSectionRendere = x.itemSectionRenderer) === null || _x$itemSectionRendere === void 0 ? void 0 : _x$itemSectionRendere.targetId)
+ === 'watch-next-feed';
+ },
+ );
+
+ if (unlockedWatchNextFeed) originalNextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(unlockedWatchNextFeed);
+
+ // Transfer video description to original response
+ const originalStructuredDescriptionContentRenderer = originalNextResponse.engagementPanels
+ .find((x) => x.engagementPanelSectionListRenderer)
+ .engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find((x) => x.expandableVideoDescriptionBodyRenderer);
+ const unlockedStructuredDescriptionContentRenderer = unlockedNextResponse.engagementPanels
+ .find((x) => x.engagementPanelSectionListRenderer)
+ .engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find((x) => x.expandableVideoDescriptionBodyRenderer);
+
+ if (unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer) {
+ originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer =
+ unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer;
+ }
+ }
+
+ /**
+ * Handles XMLHttpRequests and
+ * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
+ * - Store auth headers for the authentication of further unlock requests.
+ * - Add "content check ok" flags to request bodys
+ */
+ function handleXhrOpen(method, url, xhr) {
+ let proxyUrl = unlockGoogleVideo(url);
+ if (proxyUrl) {
+ // Exclude credentials from XMLHttpRequest
+ Object.defineProperty(xhr, 'withCredentials', {
+ set: () => {},
+ get: () => false,
+ });
+ return proxyUrl;
+ }
+
+ if (url.pathname.indexOf('/youtubei/') === 0) {
+ // Store auth headers in storage for further usage.
+ attach$4(xhr, 'setRequestHeader', ([headerName, headerValue]) => {
+ if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
+ set(headerName, headerValue);
+ }
+ });
+ }
+
+ if (Config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url.pathname)) {
+ // Add content check flags to player and next request (this will skip content warnings)
+ attach$4(xhr, 'send', (args) => {
+ if (typeof args[0] === 'string') {
+ args[0] = setContentCheckOk(args[0]);
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles Fetch requests and
+ * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
+ * - Store auth headers for the authentication of further unlock requests.
+ * - Add "content check ok" flags to request bodys
+ */
+ function handleFetchRequest(url, requestOptions) {
+ let newGoogleVideoUrl = unlockGoogleVideo(url);
+ if (newGoogleVideoUrl) {
+ // Exclude credentials from Fetch Request
+ if (requestOptions.credentials) {
+ requestOptions.credentials = 'omit';
+ }
+ return newGoogleVideoUrl;
+ }
+
+ if (url.pathname.indexOf('/youtubei/') === 0 && isObject(requestOptions.headers)) {
+ // Store auth headers in authStorage for further usage.
+ for (let headerName in requestOptions.headers) {
+ if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
+ set(headerName, requestOptions.headers[headerName]);
+ }
+ }
+ }
+
+ if (Config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url.pathname)) {
+ // Add content check flags to player and next request (this will skip content warnings)
+ requestOptions.body = setContentCheckOk(requestOptions.body);
+ }
+ }
+
+ /**
+ * If the account proxy was used to retrieve the video info, the following applies:
+ * some video files (mostly music videos) can only be accessed from IPs in the same country as the innertube api request (/youtubei/v1/player) was made.
+ * to get around this, the googlevideo URL will be replaced with a web-proxy URL in the same country (US).
+ * this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url...
+ * @returns The rewitten url (if a proxy is required)
+ */
+ function unlockGoogleVideo(url) {
+ if (Config.VIDEO_PROXY_SERVER_HOST && isGoogleVideoUrl(url)) {
+ if (isGoogleVideoUnlockRequired(url, getLastProxiedGoogleVideoId())) {
+ return proxy.getGoogleVideoUrl(url);
+ }
+ }
+ }
+
+ /**
+ * Adds `contentCheckOk` and `racyCheckOk` to the given json data (if the data contains a video id)
+ * @returns {string} The modified json
+ */
+ function setContentCheckOk(bodyJson) {
+ try {
+ let parsedBody = JSON.parse(bodyJson);
+ if (parsedBody.videoId) {
+ parsedBody.contentCheckOk = true;
+ parsedBody.racyCheckOk = true;
+ return JSON.stringify(parsedBody);
+ }
+ } catch {}
+ return bodyJson;
+ }
+
+ function processThumbnails(responseObject) {
+ const thumbnails = findNestedObjectsByAttributeNames(responseObject, ['url', 'height']);
+
+ let blurredThumbnailCount = 0;
+
+ for (const thumbnail of thumbnails) {
+ if (isThumbnailBlurred(thumbnail)) {
+ blurredThumbnailCount++;
+ thumbnail.url = thumbnail.url.split('?')[0];
+ }
+ }
+
+ info(blurredThumbnailCount + '/' + thumbnails.length + ' thumbnails detected as blurred.');
+ }
+
+ function isThumbnailBlurred(thumbnail) {
+ const hasSQPParam = thumbnail.url.indexOf('?sqp=') !== -1;
+
+ if (!hasSQPParam) {
+ return false;
+ }
+
+ const SQPLength = new URL(thumbnail.url).searchParams.get('sqp').length;
+ const isBlurred = Config.BLURRED_THUMBNAIL_SQP_LENGTHS.includes(SQPLength);
+
+ return isBlurred;
+ }
+
+ try {
+ attach$3(processYtData);
+ attach$2(processYtData);
+ attach(handleXhrOpen);
+ attach$1(handleFetchRequest);
+ } catch (err) {
+ error(err, 'Error while attaching data interceptors');
+ }
+
+ function processYtData(ytData) {
+ try {
+ // Player Unlock #1: Initial page data structure and response from `/youtubei/v1/player` XHR request
+ if (isPlayerObject(ytData) && isAgeRestricted(ytData.playabilityStatus)) {
+ unlockResponse$1(ytData);
+ } // Player Unlock #2: Embedded Player inital data structure
+ else if (isEmbeddedPlayerObject(ytData) && isAgeRestricted(ytData.previewPlayabilityStatus)) {
+ unlockResponse$1(ytData);
+ }
+ } catch (err) {
+ error(err, 'Video unlock failed');
+ }
+
+ try {
+ // Unlock sidebar watch next feed (sidebar) and video description
+ if (isWatchNextObject(ytData) && isWatchNextSidebarEmpty(ytData)) {
+ unlockResponse(ytData);
+ }
+
+ // Mobile version
+ if (isWatchNextObject(ytData.response) && isWatchNextSidebarEmpty(ytData.response)) {
+ unlockResponse(ytData.response);
+ }
+ } catch (err) {
+ error(err, 'Sidebar unlock failed');
+ }
+
+ try {
+ // Unlock blurry video thumbnails in search results
+ if (isSearchResult(ytData)) {
+ processThumbnails(ytData);
+ }
+ } catch (err) {
+ error(err, 'Thumbnail unlock failed');
+ }
+
+ return ytData;
+ }
+})();
diff --git a/web/userscripts/anti-disabled.user.js b/web/userscripts/anti-disabled.user.js
index aa37d70..106f6a9 100644
--- a/web/userscripts/anti-disabled.user.js
+++ b/web/userscripts/anti-disabled.user.js
@@ -1,15 +1,15 @@
// ==UserScript==
// @name anti-disabled
// @description Remove fuckin password and button limitation on websites
-// @version 1.2
+// @version 1.2a
// @author sysadmin_fr | https://discord.gg/ejJ4dsg
// @namespace https://gist.github.com/Albirew/
// @grant none
// @include http://*
// @include https://*
// @run-at document-start
-// @downloadURL https://gist.githubusercontent.com/Albirew/70167de32ecf5992c97c80b00584d4ee/raw/
-// @updateURL https://gist.githubusercontent.com/Albirew/70167de32ecf5992c97c80b00584d4ee/raw/
+// @downloadURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/anti-disabled.user.js
+// @updateURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/anti-disabled.user.js
// ==/UserScript==
document.querySelectorAll('button[contains(., 'disabled')]').map(el => el != null && el.disabled = false)
diff --git a/web/userscripts/stp-middle-click-hyjacking.user.js b/web/userscripts/stp-middle-click-hyjacking.user.js
new file mode 100644
index 0000000..b9c4f06
--- /dev/null
+++ b/web/userscripts/stp-middle-click-hyjacking.user.js
@@ -0,0 +1,20 @@
+// ==UserScript==
+// @name Stop Middle Click Hijacking
+// @description Prevent sites from hijacking the middle mouse button for their own purposes
+// @icon http://www.rjlsoftware.com/software/entertainment/finger/icons/finger.gif
+// @version 0.2a
+// @license GNU General Public License v3
+// @copyright 2014, Nickel
+// @grant none
+// @include *
+// @namespace https://greasyfork.org/users/10797
+// @downloadURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/stp-middle-click-hyjacking.user.js
+// @updateURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/stp-middle-click-hyjacking.user.js
+// ==/UserScript==
+
+(function(){
+ //Adapted from Chrome extension (written by petergrabs@yahoo.com)
+ //TODO: would event.preventDefault() also work??
+
+ document.addEventListener("click", function(e){ e.button===1 && e.stopPropagation(); }, true);
+})();
\ No newline at end of file
diff --git a/web/userscripts/unlazyload.user.js b/web/userscripts/unlazyload.user.js
index b927e84..28bcaf2 100644
--- a/web/userscripts/unlazyload.user.js
+++ b/web/userscripts/unlazyload.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Unlazy-Load Images
// @namespace https://greasyfork.org/en/users/85671-jcunews
-// @version 0.2b
+// @version 0.2c
// @license AGPL v3
// @author jcunews
// @description remove shitty lazyload
@@ -10,6 +10,8 @@
// @include http*://*www.webtoons.com/*
// @include *
// @grant none
+// @downloadURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/unlazyload.user.js
+// @updateURL https://git.dess.ga/Albirew/GISTS/raw/branch/main/web/userscripts/unlazyload.user.js
// ==/UserScript==
(() => {