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 = + '
\n
\n
'; + + 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== (() => {