(function () {
'use strict';
/* Setup the CSP (content security policy)
------------------------------------------------------------------------------------------ */
// Include DOM Purify (https://github.com/cure53/DOMPurify/blob/main/dist/purify.min.js)
// We're placing this inline this because fetching it from an external repo which don't have control over is likely unreliable in the long term
var Xt = Object.defineProperty; var c = (r, o) => Xt(r, "name", { value: o, configurable: !0 }); var { entries: gt, setPrototypeOf: ft, isFrozen: jt, getPrototypeOf: Vt, getOwnPropertyDescriptor: $t } = Object, { freeze: R, seal: b, create: ve } = Object, { apply: ke, construct: Ue } = typeof Reflect < "u" && Reflect; R || (R = c(function (o) { return o }, "freeze")); b || (b = c(function (o) { return o }, "seal")); ke || (ke = c(function (o, l) { for (var a = arguments.length, f = new Array(a > 2 ? a - 2 : 0), y = 2; y < a; y++)f[y - 2] = arguments[y]; return o.apply(l, f) }, "apply")); Ue || (Ue = c(function (o) { for (var l = arguments.length, a = new Array(l > 1 ? l - 1 : 0), f = 1; f < l; f++)a[f - 1] = arguments[f]; return new o(...a) }, "construct")); var fe = O(Array.prototype.forEach), qt = O(Array.prototype.lastIndexOf), ut = O(Array.prototype.pop), K = O(Array.prototype.push), Kt = O(Array.prototype.splice), me = O(String.prototype.toLowerCase), Ie = O(String.prototype.toString), Ce = O(String.prototype.match), Z = O(String.prototype.replace), Zt = O(String.prototype.indexOf), Jt = O(String.prototype.trim), D = O(Object.prototype.hasOwnProperty), S = O(RegExp.prototype.test), J = Qt(TypeError); function O(r) { return function (o) { o instanceof RegExp && (o.lastIndex = 0); for (var l = arguments.length, a = new Array(l > 1 ? l - 1 : 0), f = 1; f < l; f++)a[f - 1] = arguments[f]; return ke(r, o, a) } } c(O, "unapply"); function Qt(r) { return function () { for (var o = arguments.length, l = new Array(o), a = 0; a < o; a++)l[a] = arguments[a]; return Ue(r, l) } } c(Qt, "unconstruct"); function s(r, o) { let l = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : me; ft && ft(r, null); let a = o.length; for (; a--;) { let f = o[a]; if (typeof f == "string") { let y = l(f); y !== f && (jt(o) || (o[a] = y), f = y) } r[f] = !0 } return r } c(s, "addToSet"); function en(r) { for (let o = 0; o < r.length; o++)D(r, o) || (r[o] = null); return r } c(en, "cleanArray"); function w(r) { let o = ve(null); for (let [l, a] of gt(r)) D(r, l) && (Array.isArray(a) ? o[l] = en(a) : a && typeof a == "object" && a.constructor === Object ? o[l] = w(a) : o[l] = a); return o } c(w, "clone"); function Q(r, o) { for (; r !== null;) { let a = $t(r, o); if (a) { if (a.get) return O(a.get); if (typeof a.value == "function") return O(a.value) } r = Vt(r) } function l() { return null } return c(l, "fallbackValue"), l } c(Q, "lookupGetter"); var mt = R(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "search", "section", "select", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]), Me = R(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "enterkeyhint", "exportparts", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "inputmode", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "part", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]), we = R(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]), tn = R(["animate", "color-profile", "cursor", "discard", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]), xe = R(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "mprescripts"]), nn = R(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]), pt = R(["#text"]), dt = R(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "exportparts", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inert", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum", "part", "pattern", "placeholder", "playsinline", "popover", "popovertarget", "popovertargetaction", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "slot", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "wrap", "xmlns", "slot"]), Pe = R(["accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "mask-type", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]), Tt = R(["accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]), ue = R(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]), on = b(/\{\{[\w\W]*|[\w\W]*\}\}/gm), an = b(/<%[\w\W]*|[\w\W]*%>/gm), rn = b(/\$\{[\w\W]*/gm), sn = b(/^data-[\-\w.\u00B7-\uFFFF]+$/), ln = b(/^aria-[\-\w]+$/), ht = b(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i), cn = b(/^(?:\w+script|data):/i), fn = b(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g), At = b(/^html$/i), un = b(/^[a-z][.\w]*(-[.\w]+)+$/i), Et = Object.freeze({ __proto__: null, ARIA_ATTR: ln, ATTR_WHITESPACE: fn, CUSTOM_ELEMENT: un, DATA_ATTR: sn, DOCTYPE_NAME: At, ERB_EXPR: an, IS_ALLOWED_URI: ht, IS_SCRIPT_OR_DATA: cn, MUSTACHE_EXPR: on, TMPLIT_EXPR: rn }), ee = { element: 1, attribute: 2, text: 3, cdataSection: 4, entityReference: 5, entityNode: 6, progressingInstruction: 7, comment: 8, document: 9, documentType: 10, documentFragment: 11, notation: 12 }, mn = c(function () { return typeof window > "u" ? null : window }, "getGlobal"), pn = c(function (o, l) { if (typeof o != "object" || typeof o.createPolicy != "function") return null; let a = null, f = "data-tt-policy-suffix"; l && l.hasAttribute(f) && (a = l.getAttribute(f)); let y = "dompurify" + (a ? "#" + a : ""); try { return o.createPolicy(y, { createHTML(v) { return v }, createScriptURL(v) { return v } }) } catch { return console.warn("TrustedTypes policy " + y + " could not be created."), null } }, "_createTrustedTypesPolicy"), _t = c(function () { return { afterSanitizeAttributes: [], afterSanitizeElements: [], afterSanitizeShadowDOM: [], beforeSanitizeAttributes: [], beforeSanitizeElements: [], beforeSanitizeShadowDOM: [], uponSanitizeAttribute: [], uponSanitizeElement: [], uponSanitizeShadowNode: [] } }, "_createHooksMap"); function St() { let r = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : mn(), o = c(i => St(i), "DOMPurify"); if (o.version = "3.3.0", o.removed = [], !r || !r.document || r.document.nodeType !== ee.document || !r.Element) return o.isSupported = !1, o; let { document: l } = r, a = l, f = a.currentScript, { DocumentFragment: y, HTMLTemplateElement: v, Node: pe, Element: Fe, NodeFilter: Y, NamedNodeMap: Rt = r.NamedNodeMap || r.MozNamedAttrMap, HTMLFormElement: Ot, DOMParser: yt, trustedTypes: te } = r, X = Fe.prototype, Lt = Q(X, "cloneNode"), bt = Q(X, "remove"), Dt = Q(X, "nextSibling"), Nt = Q(X, "childNodes"), ne = Q(X, "parentNode"); if (typeof v == "function") { let i = l.createElement("template"); i.content && i.content.ownerDocument && (l = i.content.ownerDocument) } let h, j = "", { implementation: de, createNodeIterator: It, createDocumentFragment: Ct, getElementsByTagName: Mt } = l, { importNode: wt } = a, A = _t(); o.isSupported = typeof gt == "function" && typeof ne == "function" && de && de.createHTMLDocument !== void 0; let { MUSTACHE_EXPR: Te, ERB_EXPR: Ee, TMPLIT_EXPR: _e, DATA_ATTR: xt, ARIA_ATTR: Pt, IS_SCRIPT_OR_DATA: vt, ATTR_WHITESPACE: He, CUSTOM_ELEMENT: kt } = Et, { IS_ALLOWED_URI: ze } = Et, d = null, Ge = s({}, [...mt, ...Me, ...we, ...xe, ...pt]), E = null, We = s({}, [...dt, ...Pe, ...Tt, ...ue]), m = Object.seal(ve(null, { tagNameCheck: { writable: !0, configurable: !1, enumerable: !0, value: null }, attributeNameCheck: { writable: !0, configurable: !1, enumerable: !0, value: null }, allowCustomizedBuiltInElements: { writable: !0, configurable: !1, enumerable: !0, value: !1 } })), V = null, ge = null, k = Object.seal(ve(null, { tagCheck: { writable: !0, configurable: !1, enumerable: !0, value: null }, attributeCheck: { writable: !0, configurable: !1, enumerable: !0, value: null } })), Be = !0, he = !0, Ye = !1, Xe = !0, U = !1, oe = !0, x = !1, Ae = !1, Se = !1, F = !1, ie = !1, ae = !1, je = !0, Ve = !1, Ut = "user-content-", Re = !0, $ = !1, H = {}, z = null, $e = s({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]), qe = null, Ke = s({}, ["audio", "video", "img", "source", "image", "track"]), Oe = null, Ze = s({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary", "title", "value", "style", "xmlns"]), re = "http://www.w3.org/1998/Math/MathML", se = "http://www.w3.org/2000/svg", I = "http://www.w3.org/1999/xhtml", G = I, ye = !1, Le = null, Ft = s({}, [re, se, I], Ie), le = s({}, ["mi", "mo", "mn", "ms", "mtext"]), ce = s({}, ["annotation-xml"]), Ht = s({}, ["title", "style", "font", "a", "script"]), q = null, zt = ["application/xhtml+xml", "text/html"], Gt = "text/html", T = null, W = null, Wt = l.createElement("form"), Je = c(function (e) { return e instanceof RegExp || e instanceof Function }, "isRegexOrFunction"), be = c(function () { let e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; if (!(W && W === e)) { if ((!e || typeof e != "object") && (e = {}), e = w(e), q = zt.indexOf(e.PARSER_MEDIA_TYPE) === -1 ? Gt : e.PARSER_MEDIA_TYPE, T = q === "application/xhtml+xml" ? Ie : me, d = D(e, "ALLOWED_TAGS") ? s({}, e.ALLOWED_TAGS, T) : Ge, E = D(e, "ALLOWED_ATTR") ? s({}, e.ALLOWED_ATTR, T) : We, Le = D(e, "ALLOWED_NAMESPACES") ? s({}, e.ALLOWED_NAMESPACES, Ie) : Ft, Oe = D(e, "ADD_URI_SAFE_ATTR") ? s(w(Ze), e.ADD_URI_SAFE_ATTR, T) : Ze, qe = D(e, "ADD_DATA_URI_TAGS") ? s(w(Ke), e.ADD_DATA_URI_TAGS, T) : Ke, z = D(e, "FORBID_CONTENTS") ? s({}, e.FORBID_CONTENTS, T) : $e, V = D(e, "FORBID_TAGS") ? s({}, e.FORBID_TAGS, T) : w({}), ge = D(e, "FORBID_ATTR") ? s({}, e.FORBID_ATTR, T) : w({}), H = D(e, "USE_PROFILES") ? e.USE_PROFILES : !1, Be = e.ALLOW_ARIA_ATTR !== !1, he = e.ALLOW_DATA_ATTR !== !1, Ye = e.ALLOW_UNKNOWN_PROTOCOLS || !1, Xe = e.ALLOW_SELF_CLOSE_IN_ATTR !== !1, U = e.SAFE_FOR_TEMPLATES || !1, oe = e.SAFE_FOR_XML !== !1, x = e.WHOLE_DOCUMENT || !1, F = e.RETURN_DOM || !1, ie = e.RETURN_DOM_FRAGMENT || !1, ae = e.RETURN_TRUSTED_TYPE || !1, Se = e.FORCE_BODY || !1, je = e.SANITIZE_DOM !== !1, Ve = e.SANITIZE_NAMED_PROPS || !1, Re = e.KEEP_CONTENT !== !1, $ = e.IN_PLACE || !1, ze = e.ALLOWED_URI_REGEXP || ht, G = e.NAMESPACE || I, le = e.MATHML_TEXT_INTEGRATION_POINTS || le, ce = e.HTML_INTEGRATION_POINTS || ce, m = e.CUSTOM_ELEMENT_HANDLING || {}, e.CUSTOM_ELEMENT_HANDLING && Je(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck) && (m.tagNameCheck = e.CUSTOM_ELEMENT_HANDLING.tagNameCheck), e.CUSTOM_ELEMENT_HANDLING && Je(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck) && (m.attributeNameCheck = e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck), e.CUSTOM_ELEMENT_HANDLING && typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements == "boolean" && (m.allowCustomizedBuiltInElements = e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements), U && (he = !1), ie && (F = !0), H && (d = s({}, pt), E = [], H.html === !0 && (s(d, mt), s(E, dt)), H.svg === !0 && (s(d, Me), s(E, Pe), s(E, ue)), H.svgFilters === !0 && (s(d, we), s(E, Pe), s(E, ue)), H.mathMl === !0 && (s(d, xe), s(E, Tt), s(E, ue))), e.ADD_TAGS && (typeof e.ADD_TAGS == "function" ? k.tagCheck = e.ADD_TAGS : (d === Ge && (d = w(d)), s(d, e.ADD_TAGS, T))), e.ADD_ATTR && (typeof e.ADD_ATTR == "function" ? k.attributeCheck = e.ADD_ATTR : (E === We && (E = w(E)), s(E, e.ADD_ATTR, T))), e.ADD_URI_SAFE_ATTR && s(Oe, e.ADD_URI_SAFE_ATTR, T), e.FORBID_CONTENTS && (z === $e && (z = w(z)), s(z, e.FORBID_CONTENTS, T)), Re && (d["#text"] = !0), x && s(d, ["html", "head", "body"]), d.table && (s(d, ["tbody"]), delete V.tbody), e.TRUSTED_TYPES_POLICY) { if (typeof e.TRUSTED_TYPES_POLICY.createHTML != "function") throw J('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); if (typeof e.TRUSTED_TYPES_POLICY.createScriptURL != "function") throw J('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); h = e.TRUSTED_TYPES_POLICY, j = h.createHTML("") } else h === void 0 && (h = pn(te, f)), h !== null && typeof j == "string" && (j = h.createHTML("")); R && R(e), W = e } }, "_parseConfig"), Qe = s({}, [...Me, ...we, ...tn]), et = s({}, [...xe, ...nn]), Bt = c(function (e) { let t = ne(e); (!t || !t.tagName) && (t = { namespaceURI: G, tagName: "template" }); let n = me(e.tagName), u = me(t.tagName); return Le[e.namespaceURI] ? e.namespaceURI === se ? t.namespaceURI === I ? n === "svg" : t.namespaceURI === re ? n === "svg" && (u === "annotation-xml" || le[u]) : !!Qe[n] : e.namespaceURI === re ? t.namespaceURI === I ? n === "math" : t.namespaceURI === se ? n === "math" && ce[u] : !!et[n] : e.namespaceURI === I ? t.namespaceURI === se && !ce[u] || t.namespaceURI === re && !le[u] ? !1 : !et[n] && (Ht[n] || !Qe[n]) : !!(q === "application/xhtml+xml" && Le[e.namespaceURI]) : !1 }, "_checkValidNamespace"), N = c(function (e) { K(o.removed, { element: e }); try { ne(e).removeChild(e) } catch { bt(e) } }, "_forceRemove"), P = c(function (e, t) { try { K(o.removed, { attribute: t.getAttributeNode(e), from: t }) } catch { K(o.removed, { attribute: null, from: t }) } if (t.removeAttribute(e), e === "is") if (F || ie) try { N(t) } catch {} else try { t.setAttribute(e, "") } catch {} }, "_removeAttribute"), tt = c(function (e) { let t = null, n = null; if (Se) e = "" + e; else { let p = Ce(e, /^[\r\n\t ]+/); n = p && p[0] } q === "application/xhtml+xml" && G === I && (e = '
' + e + ""); let u = h ? h.createHTML(e) : e; if (G === I) try { t = new yt().parseFromString(u, q) } catch {} if (!t || !t.documentElement) { t = de.createDocument(G, "template", null); try { t.documentElement.innerHTML = ye ? j : u } catch {} } let g = t.body || t.documentElement; return e && n && g.insertBefore(l.createTextNode(n), g.childNodes[0] || null), G === I ? Mt.call(t, x ? "html" : "body")[0] : x ? t.documentElement : g }, "_initDocument"), nt = c(function (e) { return It.call(e.ownerDocument || e, e, Y.SHOW_ELEMENT | Y.SHOW_COMMENT | Y.SHOW_TEXT | Y.SHOW_PROCESSING_INSTRUCTION | Y.SHOW_CDATA_SECTION, null) }, "_createNodeIterator"), De = c(function (e) { return e instanceof Ot && (typeof e.nodeName != "string" || typeof e.textContent != "string" || typeof e.removeChild != "function" || !(e.attributes instanceof Rt) || typeof e.removeAttribute != "function" || typeof e.setAttribute != "function" || typeof e.namespaceURI != "string" || typeof e.insertBefore != "function" || typeof e.hasChildNodes != "function") }, "_isClobbered"), ot = c(function (e) { return typeof pe == "function" && e instanceof pe }, "_isNode"); function C(i, e, t) { fe(i, n => { n.call(o, e, t, W) }) } c(C, "_executeHooks"); let it = c(function (e) { let t = null; if (C(A.beforeSanitizeElements, e, null), De(e)) return N(e), !0; let n = T(e.nodeName); if (C(A.uponSanitizeElement, e, { tagName: n, allowedTags: d }), oe && e.hasChildNodes() && !ot(e.firstElementChild) && S(/<[/\w!]/g, e.innerHTML) && S(/<[/\w!]/g, e.textContent) || e.nodeType === ee.progressingInstruction || oe && e.nodeType === ee.comment && S(/<[/\w]/g, e.data)) return N(e), !0; if (!(k.tagCheck instanceof Function && k.tagCheck(n)) && (!d[n] || V[n])) { if (!V[n] && rt(n) && (m.tagNameCheck instanceof RegExp && S(m.tagNameCheck, n) || m.tagNameCheck instanceof Function && m.tagNameCheck(n))) return !1; if (Re && !z[n]) { let u = ne(e) || e.parentNode, g = Nt(e) || e.childNodes; if (g && u) { let p = g.length; for (let L = p - 1; L >= 0; --L) { let M = Lt(g[L], !0); M.__removalCount = (e.__removalCount || 0) + 1, u.insertBefore(M, Dt(e)) } } } return N(e), !0 } return e instanceof Fe && !Bt(e) || (n === "noscript" || n === "noembed" || n === "noframes") && S(/<\/no(script|embed|frames)/i, e.innerHTML) ? (N(e), !0) : (U && e.nodeType === ee.text && (t = e.textContent, fe([Te, Ee, _e], u => { t = Z(t, u, " ") }), e.textContent !== t && (K(o.removed, { element: e.cloneNode() }), e.textContent = t)), C(A.afterSanitizeElements, e, null), !1) }, "_sanitizeElements"), at = c(function (e, t, n) { if (je && (t === "id" || t === "name") && (n in l || n in Wt)) return !1; if (!(he && !ge[t] && S(xt, t))) { if (!(Be && S(Pt, t))) { if (!(k.attributeCheck instanceof Function && k.attributeCheck(t, e))) { if (!E[t] || ge[t]) { if (!(rt(e) && (m.tagNameCheck instanceof RegExp && S(m.tagNameCheck, e) || m.tagNameCheck instanceof Function && m.tagNameCheck(e)) && (m.attributeNameCheck instanceof RegExp && S(m.attributeNameCheck, t) || m.attributeNameCheck instanceof Function && m.attributeNameCheck(t, e)) || t === "is" && m.allowCustomizedBuiltInElements && (m.tagNameCheck instanceof RegExp && S(m.tagNameCheck, n) || m.tagNameCheck instanceof Function && m.tagNameCheck(n)))) return !1 } else if (!Oe[t]) { if (!S(ze, Z(n, He, ""))) { if (!((t === "src" || t === "xlink:href" || t === "href") && e !== "script" && Zt(n, "data:") === 0 && qe[e])) { if (!(Ye && !S(vt, Z(n, He, "")))) { if (n) return !1 } } } } } } } return !0 }, "_isValidAttribute"), rt = c(function (e) { return e !== "annotation-xml" && Ce(e, kt) }, "_isBasicCustomElement"), st = c(function (e) { C(A.beforeSanitizeAttributes, e, null); let { attributes: t } = e; if (!t || De(e)) return; let n = { attrName: "", attrValue: "", keepAttr: !0, allowedAttributes: E, forceKeepAttr: void 0 }, u = t.length; for (; u--;) { let g = t[u], { name: p, namespaceURI: L, value: M } = g, B = T(p), Ne = M, _ = p === "value" ? Ne : Jt(Ne); if (n.attrName = B, n.attrValue = _, n.keepAttr = !0, n.forceKeepAttr = void 0, C(A.uponSanitizeAttribute, e, n), _ = n.attrValue, Ve && (B === "id" || B === "name") && (P(p, e), _ = Ut + _), oe && S(/((--!?|])>)|<\/(style|title|textarea)/i, _)) { P(p, e); continue } if (B === "attributename" && Ce(_, "href")) { P(p, e); continue } if (n.forceKeepAttr) continue; if (!n.keepAttr) { P(p, e); continue } if (!Xe && S(/\/>/i, _)) { P(p, e); continue } U && fe([Te, Ee, _e], ct => { _ = Z(_, ct, " ") }); let lt = T(e.nodeName); if (!at(lt, B, _)) { P(p, e); continue } if (h && typeof te == "object" && typeof te.getAttributeType == "function" && !L) switch (te.getAttributeType(lt, B)) { case "TrustedHTML": { _ = h.createHTML(_); break } case "TrustedScriptURL": { _ = h.createScriptURL(_); break } }if (_ !== Ne) try { L ? e.setAttributeNS(L, p, _) : e.setAttribute(p, _), De(e) ? N(e) : ut(o.removed) } catch { P(p, e) } } C(A.afterSanitizeAttributes, e, null) }, "_sanitizeAttributes"), Yt = c(function i(e) { let t = null, n = nt(e); for (C(A.beforeSanitizeShadowDOM, e, null); t = n.nextNode();)C(A.uponSanitizeShadowNode, t, null), it(t), st(t), t.content instanceof y && i(t.content); C(A.afterSanitizeShadowDOM, e, null) }, "_sanitizeShadowDOM"); return o.sanitize = function (i) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, t = null, n = null, u = null, g = null; if (ye = !i, ye && (i = ""), typeof i != "string" && !ot(i)) if (typeof i.toString == "function") { if (i = i.toString(), typeof i != "string") throw J("dirty is not a string, aborting") } else throw J("toString is not a function"); if (!o.isSupported) return i; if (Ae || be(e), o.removed = [], typeof i == "string" && ($ = !1), $) { if (i.nodeName) { let M = T(i.nodeName); if (!d[M] || V[M]) throw J("root node is forbidden and cannot be sanitized in-place") } } else if (i instanceof pe) t = tt(""), n = t.ownerDocument.importNode(i, !0), n.nodeType === ee.element && n.nodeName === "BODY" || n.nodeName === "HTML" ? t = n : t.appendChild(n); else { if (!F && !U && !x && i.indexOf("<") === -1) return h && ae ? h.createHTML(i) : i; if (t = tt(i), !t) return F ? null : ae ? j : "" } t && Se && N(t.firstChild); let p = nt($ ? i : t); for (; u = p.nextNode();)it(u), st(u), u.content instanceof y && Yt(u.content); if ($) return i; if (F) { if (ie) for (g = Ct.call(t.ownerDocument); t.firstChild;)g.appendChild(t.firstChild); else g = t; return (E.shadowroot || E.shadowrootmode) && (g = wt.call(a, g, !0)), g } let L = x ? t.outerHTML : t.innerHTML; return x && d["!doctype"] && t.ownerDocument && t.ownerDocument.doctype && t.ownerDocument.doctype.name && S(At, t.ownerDocument.doctype.name) && (L = "` + L), U && fe([Te, Ee, _e], M => { L = Z(L, M, " ") }), h && ae ? h.createHTML(L) : L }, o.setConfig = function () { let i = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; be(i), Ae = !0 }, o.clearConfig = function () { W = null, Ae = !1 }, o.isValidAttribute = function (i, e, t) { W || be({}); let n = T(i), u = T(e); return at(n, u, t) }, o.addHook = function (i, e) { typeof e == "function" && K(A[i], e) }, o.removeHook = function (i, e) { if (e !== void 0) { let t = qt(A[i], e); return t === -1 ? void 0 : Kt(A[i], t, 1)[0] } return ut(A[i]) }, o.removeHooks = function (i) { A[i] = [] }, o.removeAllHooks = function () { A = _t() }, o } c(St, "createDOMPurify"); var DOMPurify = St();
// Create a custom CSP policy (using DOM Purify for added security)
let goodTube_csp = false;
if (window.trustedTypes && window.trustedTypes.createPolicy && DOMPurify) {
goodTube_csp = window.trustedTypes.createPolicy("GoodTubePolicy", {
createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true })
});
}
/* Helper functions
------------------------------------------------------------------------------------------ */
// Setup GET parameters
function goodTube_helper_setupGetParams() {
let getParams = {};
document.location.search.replace(/\??(?:([^=]+)=([^&]*)&?)/g, function () {
function decode(s) {
return decodeURIComponent(s.split("+").join(" "));
}
getParams[decode(arguments[1])] = decode(arguments[2]);
});
// For some users, the URL will contain the video ID as part of the URL
// Example - "/watch/xxxx" or "/live/xxxx"
// In this case, we want to add it manually as "v" (just like for /watch?v=xxxx)
if (goodTube_helper_watchingVideo() && typeof getParams['v'] === 'undefined') {
let splitString = '';
if (window.location.href.indexOf('/watch/') !== -1) {
splitString = '/watch/';
}
else {
splitString = '/live/';
}
let bits = window.location.href.split(splitString);
if (bits.length === 2) {
let endBits = bits[1].split('?');
getParams['v'] = endBits[endBits.length - 1];
}
}
return getParams;
}
// Set a cookie
function goodTube_helper_setCookie(name, value, days = 399) {
// Force new cookie names, we had the path attribute wrong...sorry all this will reset your settings (22/10/2025)
name = name + '_new';
document.cookie = name + "=" + encodeURIComponent(value) + ";SameSite=Lax;path=/;max-age=" + (days * 24 * 60 * 60);
}
// Get a cookie
function goodTube_helper_getCookie(name) {
// Force new cookie names, we had the path attribute wrong...sorry all this will reset your settings (22/10/2025)
name = name + '_new';
// Split the cookie string and get all individual name=value pairs in an array
let cookies = document.cookie.split(";");
// Loop through the array elements
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].split("=");
// Removing whitespace at the beginning of the cookie name and compare it with the given string
if (name == cookie[0].trim()) {
// Decode the cookie value and return
return decodeURIComponent(cookie[1]);
}
}
// Return null if not found
return null;
}
// Simulate a click (without changing focus)
function goodTube_helper_click(element) {
if (element) {
element.dispatchEvent(new PointerEvent('mousedown', { bubbles: true, cancelable: true, button: 0 }));
element.dispatchEvent(new PointerEvent('click', { bubbles: true, cancelable: true, button: 0 }));
element.dispatchEvent(new PointerEvent('mouseup', { bubbles: true, cancelable: true, button: 0 }));
}
}
// Add a CSS class to show or hide elements
function goodTube_helper_showHide_init() {
let style = document.createElement('style');
style.textContent = `
.goodTube_hidden {
display: none !important;
}
`;
document.head.appendChild(style);
}
// Hide an element
function goodTube_helper_hideElement(element) {
if (element && !element.classList.contains('goodTube_hidden')) {
element.classList.add('goodTube_hidden');
}
}
// Show an element
function goodTube_helper_showElement(element) {
if (element && element.classList.contains('goodTube_hidden')) {
element.classList.remove('goodTube_hidden');
}
}
// Check if we're watching a video
function goodTube_helper_watchingVideo() {
// If the URL contains "/watch/" or "/watch?" or "/live/", we're viewing a video
if (window.location.href.indexOf('/watch/') !== -1 || window.location.href.indexOf('/watch?') !== -1 || window.location.href.indexOf('/live/') !== -1) {
return true;
}
// Otherwise, we're not viewing a video
else {
return false;
}
}
// Check if ads are showing
function goodTube_helper_adsShowing() {
// If we're viewing a video
if (goodTube_helper_watchingVideo()) {
// Get the ads DOM elements
let adsElement = document.querySelector('.video-ads');
let sponsoredAdsElement = document.querySelector('.ad-simple-attributed-string');
// If ads are showing
if ((adsElement && adsElement.checkVisibility()) || (sponsoredAdsElement && sponsoredAdsElement.checkVisibility())) {
return true;
}
// Otherwise, ads are not showing
else {
return false;
}
}
}
// Set HTML using our CSP policy
function goodTube_helper_innerHTML(element, html) {
// If our CSP policy exists, sanitise the HTML using it
if (goodTube_csp) {
html = goodTube_csp.createHTML(html);
}
// Run the innerHTML function
element.innerHTML = html;
}
function goodTube_helper_insertAdjacentHTML(element, position, html) {
// If our CSP policy exists, sanitise the HTML using it
if (goodTube_csp) {
html = goodTube_csp.createHTML(html);
}
// Run the insertAdjacentHTML function
element.insertAdjacentHTML(position, html);
}
// Trigger a keyboard shortcut
function goodTube_helper_shortcut(shortcut) {
let theKey = false;
let keyCode = false;
let shiftKey = false;
if (shortcut === 'next') {
theKey = 'n';
keyCode = 78;
shiftKey = true;
}
else if (shortcut === 'previous') {
theKey = 'p';
keyCode = 80;
shiftKey = true;
}
let e = false;
e = new window.KeyboardEvent('focus', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('keydown', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('beforeinput', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('keypress', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('input', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('change', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
e = new window.KeyboardEvent('keyup', {
bubbles: true,
key: theKey,
keyCode: keyCode,
shiftKey: shiftKey,
charCode: 0,
});
document.dispatchEvent(e);
}
/* Global variables
------------------------------------------------------------------------------------------ */
// A reference to our player's wrapper
let goodTube_playerWrapper = false;
// A reference to our player's iframe
let goodTube_player = false;
// The page api
let goodTube_page_api = false;
// The iframe api
let goodTube_iframe_api = false;
// Are we in picture in picture?
let goodTube_pip = false;
// Are we syncing the main Youtube player?
let goodTube_syncingPlayer = false;
// A reference to the previous URL (used to detect when the page changes)
let goodTube_previousUrl = false;
// Have we already turned off Youtube's default autoplay?
let goodTube_turnedOffAutoplay = false;
// Have we already redirected away from a short?
let goodTube_redirectHappened = false;
// Is this the first video we're loading?
let goodTube_firstLoad = true;
// Has the proxy iframe loaded?
let goodTube_proxyIframeLoaded = false;
// Has the player iframe loaded?
let goodTube_playerIframeLoaded = false;
// Hold the playlist information
let goodTube_playlist = false;
let goodTube_playlistIndex = 0;
// Is the "hide and mute ads" fallback active?
let goodTube_fallback = false;
// Is the tab in focus?
let goodTube_tabInFocus = document.hasFocus();
// Are shorts enabled?
let goodTube_shorts = goodTube_helper_getCookie('goodTube_shorts');
if (!goodTube_shorts) {
goodTube_helper_setCookie('goodTube_shorts', 'false');
goodTube_shorts = 'false';
}
// Are info cards enabled?
let goodTube_hideInfoCards = goodTube_helper_getCookie('goodTube_hideInfoCards');
if (!goodTube_hideInfoCards) {
goodTube_helper_setCookie('goodTube_hideInfoCards', 'false');
goodTube_hideInfoCards = 'false';
}
// Is the end screen enabled (suggested videos)?
let goodTube_hideEndScreen = goodTube_helper_getCookie('goodTube_hideEndScreen');
if (!goodTube_hideEndScreen) {
goodTube_helper_setCookie('goodTube_hideEndScreen', 'false');
goodTube_hideEndScreen = 'false';
}
// Are suggested videos enabled (sidebar)?
let goodTube_hideSuggestedVideos = goodTube_helper_getCookie('goodTube_hideSuggestedVideos');
if (!goodTube_hideSuggestedVideos) {
goodTube_helper_setCookie('goodTube_hideSuggestedVideos', 'false');
goodTube_hideSuggestedVideos = 'false';
}
// Are comments enabled?
let goodTube_hideComments = goodTube_helper_getCookie('goodTube_hideComments');
if (!goodTube_hideComments) {
goodTube_helper_setCookie('goodTube_hideComments', 'false');
goodTube_hideComments = 'false';
}
// Are AI summaries enabled?
let goodTube_hideAiSummaries = goodTube_helper_getCookie('goodTube_hideAiSummaries');
if (!goodTube_hideAiSummaries) {
goodTube_helper_setCookie('goodTube_hideAiSummaries', 'false');
goodTube_hideAiSummaries = 'false';
}
// Are members only videos enabled?
let goodTube_hideMembersOnlyVideos = goodTube_helper_getCookie('goodTube_hideMembersOnlyVideos');
if (!goodTube_hideMembersOnlyVideos) {
goodTube_helper_setCookie('goodTube_hideMembersOnlyVideos', 'false');
goodTube_hideMembersOnlyVideos = 'false';
}
// Always play videos from the start?
let goodTube_alwaysStart = goodTube_helper_getCookie('goodTube_alwaysStart');
if (!goodTube_alwaysStart) {
goodTube_helper_setCookie('goodTube_alwaysStart', 'false');
goodTube_alwaysStart = 'false';
}
// Use a black background for the video player?
let goodTube_blackBackground = goodTube_helper_getCookie('goodTube_blackBackground');
if (!goodTube_blackBackground) {
goodTube_helper_setCookie('goodTube_blackBackground', 'true');
goodTube_blackBackground = 'true';
}
// Videos per row on the home page
let goodTube_videosPerRow = goodTube_helper_getCookie('goodTube_videosPerRow');
if (!goodTube_videosPerRow) {
goodTube_helper_setCookie('goodTube_videosPerRow', 'default');
goodTube_videosPerRow = 'default';
}
// Is autoplay turned on?
let goodTube_autoplay = goodTube_helper_getCookie('goodTube_autoplay');
if (!goodTube_autoplay) {
goodTube_helper_setCookie('goodTube_autoplay', 'true');
goodTube_autoplay = 'true';
}
// Get the playback speed to restore it
let goodTube_playbackSpeed = goodTube_helper_getCookie('goodTube_playbackSpeed');
if (!goodTube_playbackSpeed) {
goodTube_playbackSpeed = '1';
}
// Fetch the GET params
let goodTube_getParams = goodTube_helper_setupGetParams();
/* Youtube functions
------------------------------------------------------------------------------------------ */
// Hide page elements
function goodTube_youtube_hidePageElements() {
// Hide ads
let cssOutput = `
.ytd-search ytd-shelf-renderer,
ytd-reel-shelf-renderer,
ytd-merch-shelf-renderer,
ytd-action-companion-ad-renderer,
ytd-display-ad-renderer,
ytd-video-masthead-ad-advertiser-info-renderer,
ytd-video-masthead-ad-primary-video-renderer,
ytd-in-feed-ad-layout-renderer,
ytd-ad-slot-renderer,
ytd-statement-banner-renderer,
ytd-banner-promo-renderer-background
ytd-ad-slot-renderer,
ytd-in-feed-ad-layout-renderer,
ytd-engagement-panel-section-list-renderer:not(.ytd-popup-container):not([target-id='engagement-panel-clip-create']):not(.ytd-shorts),
ytd-compact-video-renderer:has(.goodTube_hidden),
ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer)
.ytd-video-masthead-ad-v3-renderer,
div#root.style-scope.ytd-display-ad-renderer.yt-simple-endpoint,
div#sparkles-container.style-scope.ytd-promoted-sparkles-web-renderer,
div#main-container.style-scope.ytd-promoted-video-renderer,
div#player-ads.style-scope.ytd-watch-flexy,
#clarify-box,
ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer),
ytm-rich-shelf-renderer,
ytm-search ytm-shelf-renderer,
ytm-button-renderer.icon-avatar_logged_out,
ytm-companion-slot,
ytm-reel-shelf-renderer,
ytm-merch-shelf-renderer,
ytm-action-companion-ad-renderer,
ytm-display-ad-renderer,
ytm-rich-section-renderer,
ytm-video-masthead-ad-advertiser-info-renderer,
ytm-video-masthead-ad-primary-video-renderer,
ytm-in-feed-ad-layout-renderer,
ytm-ad-slot-renderer,
ytm-statement-banner-renderer,
ytm-banner-promo-renderer-background
ytm-ad-slot-renderer,
ytm-in-feed-ad-layout-renderer,
ytm-compact-video-renderer:has(.goodTube_hidden),
ytm-rich-item-renderer:has(> #content > ytm-ad-slot-renderer)
.ytm-video-masthead-ad-v3-renderer,
div#root.style-scope.ytm-display-ad-renderer.yt-simple-endpoint,
div#sparkles-container.style-scope.ytm-promoted-sparkles-web-renderer,
div#main-container.style-scope.ytm-promoted-video-renderer,
div#player-ads.style-scope.ytm-watch-flexy,
ytd-compact-movie-renderer,
yt-about-this-ad-renderer,
masthead-ad,
ad-slot-renderer,
yt-mealbar-promo-renderer,
statement-banner-style-type-compact,
ytm-promoted-sparkles-web-renderer,
tp-yt-iron-overlay-backdrop,
#masthead-ad
{
display: none !important;
}
.style-scope[page-subtype='channels'] ytd-shelf-renderer,
.style-scope[page-subtype='channels'] ytm-shelf-renderer {
display: block !important;
}
`;
// Debug message
console.log('[GoodTube] Ads removed');
// Hide the main Youtube player
cssOutput += `
body:not(.goodTube_fallback) #player:not(.ytd-shorts):not(.ytd-channel-video-player-renderer),
body:not(.goodTube_fallback) #player-full-bleed-container {
visibility: hidden !important;
}
`;
// Hide the miniplayer
cssOutput += `
ytd-miniplayer,
.ytp-miniplayer-button {
display: none !important;
}
`;
// Hide shorts if they're not enabled
if (goodTube_shorts === 'false') {
cssOutput += `
ytm-pivot-bar-item-renderer:has(> .pivot-shorts),
ytd-rich-section-renderer,
grid-shelf-view-model {
display: none !important;
}
`;
// Debug message
console.log('[GoodTube] Shorts removed');
}
// Otherwise, allow shorts in watch history
else {
cssOutput += `
ytd-item-section-renderer[page-subtype='history'] ytd-reel-shelf-renderer {
display: block !important;
}
`;
}
// Hide suggested videos if they're not enabled
if (goodTube_hideSuggestedVideos === 'true') {
cssOutput += `
/* Hide suggested videos */
ytd-watch-flexy #secondary #related {
display: none !important;
}
/* Hide full sidebar if not playlist */
ytd-watch-flexy #secondary:not(:has(ytd-playlist-panel-video-renderer)) {
display: none !important;
}
`;
// Debug message
console.log('[GoodTube] Suggested videos removed');
}
// Hide comments if they're not enabled
if (goodTube_hideComments === 'true') {
cssOutput += `
ytd-item-section-renderer.ytd-comments,
#comments-button,
#shorts-panel-container ytd-engagement-panel-section-list-renderer {
display: none !important;
}
`;
// Debug message
console.log('[GoodTube] Comments removed');
}
// Hide AI summaries if they're not enabled
if (goodTube_hideAiSummaries === 'true') {
cssOutput += `
ytd-expandable-metadata-renderer[has-video-summary] {
display: none !important;
}
`;
// Debug message
console.log('[GoodTube] AI summaries removed');
}
// Hide members only videos if they're not enabled
if (goodTube_hideMembersOnlyVideos === 'true') {
// Debug message
console.log('[GoodTube] Members only videos removed');
}
// Videos per row on the home page (check if default, also make sure it's a number)
if (goodTube_videosPerRow !== 'default' && goodTube_videosPerRow == parseFloat(goodTube_videosPerRow)) {
// Debug message
console.log('[GoodTube] Videos per row on the home page set to ' + goodTube_videosPerRow);
let fixedWidthPercentage = (100 / parseFloat(goodTube_videosPerRow)) + '%';
cssOutput += `
ytd-rich-item-renderer[rendered-from-rich-grid] {
width: calc(` + fixedWidthPercentage + ` - ((var(--ytd-rich-grid-item-margin) / 2)) / ` + parseFloat(goodTube_videosPerRow - 1) + ` * ` + (parseFloat(goodTube_videosPerRow - 1) * 2) + ` ) !important;
width: calc(` + fixedWidthPercentage + ` - ((var(--ytd-rich-grid-item-margin) / 2)) / ` + parseFloat(goodTube_videosPerRow - 1) + ` * ` + (parseFloat(goodTube_videosPerRow - 1) * 2) + ` ) !important;
margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important;
margin-right: calc(var(--ytd-rich-grid-item-margin) / 2) !important;
}
#contents.ytd-rich-grid-renderer {
padding-right: 24px !important;
box-sizing: border-box !important;
}
`;
}
// Add the styles to the page
let style = document.createElement('style');
style.textContent = cssOutput;
document.head.appendChild(style);
}
// Hide members only videos
function goodTube_youtube_hideMembersOnlyVideos() {
let videos = document.querySelectorAll('ytd-rich-item-renderer:not(.goodTube_checked)');
videos.forEach((element) => {
if (element.innerHTML.toLowerCase().indexOf('members only') !== -1) {
goodTube_helper_hideElement(element);
}
// Mark this element as checked to save on resources
element.classList.add('goodTube_checked');
});
}
// Hide shorts (real time)
function goodTube_youtube_hideShortsRealTime() {
// If shorts are enabled, don't do anything
if (goodTube_shorts === 'true') {
return;
}
// Redirect from any short to the home page
if (window.location.href.indexOf('/shorts') !== -1 && !goodTube_redirectHappened) {
window.location.href = 'https://youtube.com';
goodTube_redirectHappened = true;
}
// Hide shorts links (we can't mark these as "checked" to save on resources, as the URLs seem to change over time)
let shortsLinks = document.querySelectorAll('a:not(.goodTube_hidden)');
shortsLinks.forEach((element) => {
if (element.href.indexOf('shorts/') !== -1) {
goodTube_helper_hideElement(element);
goodTube_helper_hideElement(element.closest('ytd-video-renderer'));
goodTube_helper_hideElement(element.closest('ytd-compact-video-renderer'));
goodTube_helper_hideElement(element.closest('ytd-rich-grid-media'));
}
});
// Hide shorts buttons
let shortsButtons = document.querySelectorAll('yt-chip-cloud-chip-renderer:not(.goodTube_hidden):not(.goodTube_checked), yt-tab-shape:not(.goodTube_hidden):not(.goodTube_checked), ytd-guide-entry-renderer:not(.goodTube_checked)');
shortsButtons.forEach((element) => {
if (element.innerHTML.toLowerCase().indexOf('shorts') !== -1) {
goodTube_helper_hideElement(element);
}
// Mark this element as checked to save on resources
element.classList.add('goodTube_checked');
});
}
// Support timestamp links in comments
function goodTube_youtube_timestampLinks() {
// Links in video description and comments
let timestampLinks = document.querySelectorAll('#description a, ytd-comments .yt-core-attributed-string a, ytm-expandable-video-description-body-renderer a, .comment-content a');
// For each link
timestampLinks.forEach((element) => {
// Make sure we've not touched it yet, this stops doubling up on event listeners
if (!element.classList.contains('goodTube_timestampLink') && element.getAttribute('href') && element.getAttribute('href').indexOf(goodTube_getParams['v']) !== -1) {
element.classList.add('goodTube_timestampLink');
// Add the event listener to send our player to the correct time
element.addEventListener('click', function () {
// Define the time to skip to
let time = 0;
// Get the time from the link (if it exstis)
let bits = element.getAttribute('href').split('t=');
if (typeof bits[1] !== 'undefined') {
time = parseFloat(bits[1].replace('s', ''));
}
// Skip to the time
goodTube_player_skipTo(time);
});
}
});
}
// Mute and pause all Youtube videos
let goodTube_youtube_pauseMuteVideos_timeout = setTimeout(() => {}, 0);
function goodTube_youtube_pauseMuteVideos() {
// IF if shorts are enabled and we're viewing a short
if (goodTube_shorts === 'true' && window.location.href.indexOf('/shorts') !== -1) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_youtube_pauseMuteVideos_timeout);
// Loop this function
goodTube_youtube_pauseMuteVideos_timeout = setTimeout(goodTube_youtube_pauseMuteVideos, 100);
// Don't pause or mute videos
return;
}
// Pause and mute all HTML videos on the page
let youtubeVideos = document.querySelectorAll('video');
youtubeVideos.forEach((video) => {
// If the "hide the mute" ads fallback is active
if (goodTube_fallback) {
// If the video is playing and it's NOT the main player
if (!video.paused && !video.closest('#movie_player')) {
// Pause and mute the video
video.muted = true;
video.volume = 0;
video.pause();
}
}
// Otherwise, the "hide and mute" ads fallback is inactive
else {
if (
!video.paused &&
(!goodTube_syncingPlayer && video.closest('#movie_player'))
&&
!video.closest('#inline_player')
) {
// Mute the video
video.muted = true;
video.volume = 0;
// If ads are not showing OR it's not the main player AND not the inline player
if (!goodTube_helper_adsShowing() || !video.closest('#movie_player')) {
// Pause the video
video.pause();
// Restore the playback rate
video.playbackRate = goodTube_playbackSpeed;
}
// Otherwise, it's the main player and ads are showing
else {
// Play the video
video.play();
// DISABLE FOR NOW, THIS MAY BE TRIGGERING DETECTION
// // Speed up to 2x (any faster is detected by Youtube)
// video.playbackRate = 2;
}
}
}
});
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_youtube_pauseMuteVideos_timeout);
// Loop this function
goodTube_youtube_pauseMuteVideos_timeout = setTimeout(goodTube_youtube_pauseMuteVideos, 100);
}
// Turn off autoplay
function goodTube_youtube_turnOffAutoplay() {
// If we've already turned off autoplay, just return
if (goodTube_turnedOffAutoplay) {
return;
}
// Target the autoplay button
let autoplayButton = document.querySelector('#movie_player .ytp-autonav-toggle-button');
// If we found it
if (autoplayButton) {
// Turn off autoplay
if (autoplayButton.getAttribute('aria-checked') === 'true') {
goodTube_helper_click(autoplayButton);
}
// Set a variable if autoplay has been turned off
goodTube_turnedOffAutoplay = true;
}
}
// Remove the "are you still watching" popup
function goodTube_youtube_removeAreYouStillWatchingPopup() {
// Get all the dialogue boxes
let dialogueBoxes = document.querySelectorAll('yt-confirm-dialog-renderer');
// For each dialogue box
dialogueBoxes.forEach((dialogueBox) => {
// If it has the correct text
if (dialogueBox.innerHTML.indexOf('Video paused. Continue watching?') !== -1) {
// Find the confirm button
let confirmButton = dialogueBox.querySelector('#confirm-button');
// If we found the confirm button
if (confirmButton) {
// Click it
goodTube_helper_click(confirmButton);
}
}
});
}
// Set the video aspect ratio
function goodTube_youtube_setAspectRatio(widthRatio, heightRatio) {
// Make sure we've been passed valid data
if (!widthRatio || !heightRatio) {
return;
}
// Target the aspect ratio element with the CSS variables
let variableElement = document.querySelector('ytd-watch-flexy');
// If we found the element, we're watching a video and the "hide and mute ads" fallback is inactive
if (variableElement && goodTube_helper_watchingVideo() && !goodTube_fallback) {
// Set the aspect ratio
variableElement.style.setProperty("--ytd-watch-flexy-width-ratio", widthRatio);
variableElement.style.setProperty("--ytd-watch-flexy-height-ratio", heightRatio);
}
}
// Unset the video aspect ratio
function goodTube_youtube_unsetAspectRatio() {
// Target the aspect ratio element with the CSS variables
let variableElement = document.querySelector('ytd-watch-flexy');
// If we found the aspect ratio element
if (variableElement) {
// Remove the aspect ratio
variableElement.style.removeProperty("--ytd-watch-flexy-width-ratio");
variableElement.style.removeProperty("--ytd-watch-flexy-height-ratio");
}
}
/* Player functions
------------------------------------------------------------------------------------------ */
// Init player
let goodTube_player_init_timeout = setTimeout(() => {}, 0);
function goodTube_player_init() {
// Get the page API
goodTube_page_api = document.getElementById('movie_player');
// Get the video data to check loading state
let videoData = false;
if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function') {
videoData = goodTube_page_api.getVideoData();
}
// Keep trying to get the frame API until it exists
if (!videoData) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_init_timeout);
// Create a new timeout
goodTube_player_init_timeout = setTimeout(goodTube_player_init, 100);
return;
}
// Use a black background for the video player? (This is a setting)
let backgroundColor = 'transparent';
if (goodTube_blackBackground === 'true') {
backgroundColor = '#000000';
}
// Add CSS styles for the player
let style = document.createElement('style');
style.textContent = `
/* Player wrapper */
#goodTube_playerWrapper {
border-radius: 12px;
background: ` + backgroundColor + `;
position: absolute;
top: 0;
left: 0;
z-index: 999;
overflow: hidden;
}
/* Theater mode */
#goodTube_playerWrapper.goodTube_theater {
border-radius: 0;
background: #000000;
}
/* No black background on dark theme */
html[darker-dark-theme][dark] #goodTube_playerWrapper {
background: transparent;
}
/* Fix size of Youtube player (this has to do with us setting the aspect ratio inside "goodTube_youtube_setAspectRatio") */
body:not(.goodTube_fallback) #primary.ytd-watch-flexy {
max-width: calc(177.77777778vh - var(--ytd-watch-flexy-masthead-height) * 1.7777777778 - var(--ytd-margin-6x) * 1.7777777778 - var(--ytd-watch-flexy-space-below-player) * 1.7777777778) !important;
min-width: calc(var(--ytd-watch-flexy-min-player-height) * 1.7777777778) !important;
}
`;
document.head.appendChild(style);
// Setup player layout
let playerWrapper = document.createElement('div');
playerWrapper.id = 'goodTube_playerWrapper';
playerWrapper.classList.add('goodTube_hidden');
// Add player to the page
document.body.appendChild(playerWrapper);
// Add video iframe embed (via proxy iframe)
let proxyIframe = document.createElement('iframe');
proxyIframe.src = 'https://wikipedia.org/wiki/Bruce_Lee?goodTubeProxy=1';
proxyIframe.setAttribute('width', '100%');
proxyIframe.setAttribute('height', '100%');
proxyIframe.setAttribute('frameborder', '0');
proxyIframe.setAttribute('scrolling', 'yes');
proxyIframe.setAttribute('allow', 'accelerometer *; autoplay *; clipboard-write *; encrypted-media *; gyroscope *; picture-in-picture *; web-share *;');
proxyIframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
proxyIframe.setAttribute('allowfullscreen', true);
proxyIframe.style.display = 'none';
playerWrapper.appendChild(proxyIframe);
// Expose these globally
goodTube_playerWrapper = playerWrapper;
goodTube_player = proxyIframe;
// Run the actions every 100ms
goodTube_actions();
setInterval(goodTube_actions, 100);
}
// Position and size the player
let goodTube_clearedPlayer = false;
function goodTube_player_positionAndSize() {
// If the "hide and mute ads" fallback is inactive
if (goodTube_fallback) {
if (!goodTube_clearedPlayer) {
// Hide and clear the embedded player
goodTube_player_clear(true);
goodTube_clearedPlayer = true;
}
}
// Otherwise, the "hide and mute ads" fallback is inactive
else {
goodTube_clearedPlayer = false;
// Get the Youtube player
// We target 4 elements here, it seems to change for different users? Weird stuff.
let youtubePlayer = document.querySelector('#player.ytd-watch-flexy');
if (!youtubePlayer || youtubePlayer.offsetHeight <= 0) {
youtubePlayer = document.querySelector('#ytd-player');
}
if (!youtubePlayer || youtubePlayer.offsetHeight <= 0) {
youtubePlayer = document.querySelector('.player-size');
}
// This element helps during the loading of the page (if we use it we see the video a little sooner, which is nice)
if (!youtubePlayer || youtubePlayer.offsetHeight <= 0) {
youtubePlayer = document.querySelector('.html5-video-player');
}
// If we found the Youtube player
if (youtubePlayer && youtubePlayer.offsetHeight > 0) {
// Make our custom player match the position of the Youtube player
// Note: Our custom player uses "position: absolute" so take into account the window scroll
let rect = youtubePlayer.getBoundingClientRect();
goodTube_playerWrapper.style.top = (rect.top + window.scrollY) + 'px';
goodTube_playerWrapper.style.left = (rect.left + window.scrollX) + 'px';
// Make our custom player match the size of the Youtube player
goodTube_playerWrapper.style.width = youtubePlayer.offsetWidth + 'px';
goodTube_playerWrapper.style.height = youtubePlayer.offsetHeight + 'px';
// Show the GoodTube player
goodTube_helper_showElement(goodTube_playerWrapper);
}
}
}
// Populate the playlist info
let goodTube_player_populatePlaylistInfo_timeout = setTimeout(() => {}, 0);
function goodTube_player_populatePlaylistInfo() {
// Re fetch the page API
goodTube_page_api = document.getElementById('movie_player');
// Get the video data
let videoData = false;
let videoId = false;
if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function') {
videoData = goodTube_page_api.getVideoData();
videoId = videoData.video_id;
}
// If the correct video hasn't loaded yet (based on the ID in the query params)
if (!videoData || videoId !== goodTube_getParams['v']) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_populatePlaylistInfo_timeout);
// Try again
goodTube_player_populatePlaylistInfo_timeout = setTimeout(goodTube_player_populatePlaylistInfo, 100);
// Don't do anything else
return;
}
// Make sure we have access to the page API
if (goodTube_page_api && typeof goodTube_page_api.getPlaylist === 'function' && typeof goodTube_page_api.getPlaylistIndex === 'function') {
goodTube_playlist = goodTube_page_api.getPlaylist();
goodTube_playlistIndex = goodTube_page_api.getPlaylistIndex();
// If the playlist info isn't ready yet
if (!goodTube_playlist) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_populatePlaylistInfo_timeout);
// Try again
goodTube_player_populatePlaylistInfo_timeout = setTimeout(goodTube_player_populatePlaylistInfo, 100);
// Don't do anything else
return;
}
}
// Otherwise, we don't have access to the frame API
else {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_populatePlaylistInfo_timeout);
// Try again
goodTube_player_populatePlaylistInfo_timeout = setTimeout(goodTube_player_populatePlaylistInfo, 100);
// Don't do anything else
return;
}
}
// Load a video
let goodTube_player_load_timeout = setTimeout(() => {}, 0);
function goodTube_player_load() {
// Reset the "hide and mute ads" state (this ensures the fallback will refresh for each new video)
goodTube_hideAndMuteAds_state = '';
// Pause the video first (this helps to prevent audio flashes)
goodTube_player_pause();
// Make sure the proxy iframe has loaded
if (!goodTube_proxyIframeLoaded) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_load_timeout);
// Create a new timeout to try again
goodTube_player_load_timeout = setTimeout(goodTube_player_load, 100);
// Don't do anything else
return;
}
// Setup the starting time
let startTime = 0;
// Include the startime time from query params (if enabled)
if (goodTube_alwaysStart === 'false') {
if (typeof goodTube_getParams['t'] !== 'undefined') {
startTime = parseFloat(goodTube_getParams['t'].replace('s', ''));
}
}
// If we're viewing a playlist
let playlist = 'false';
if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
// Populate the GET params below to let the iframe know we're viewing a playlist
playlist = 'true';
// Populate the playlist info
goodTube_player_populatePlaylistInfo();
}
// Otherwise, remove playlist info
else {
goodTube_playlist = false;
goodTube_playlistIndex = 0;
}
// If we're loading for the first time
if (goodTube_firstLoad) {
// If we're not viewing a video
if (!goodTube_helper_watchingVideo()) {
// Clear and hide the player
goodTube_player_clear();
}
// Include the start time if it exists
let startTimeParam = '';
if (startTime > 0) {
startTimeParam = '&start=' + startTime;
}
// Set the video source
goodTube_player.contentWindow.postMessage('goodTube_src_https://www.youtube.com/embed/' + goodTube_getParams['v'] + '?goodTubeEmbed=1&autoplay=1&goodTube_playlist=' + playlist + '&goodTube_autoplay=' + goodTube_autoplay + '&goodTube_playbackSpeed=' + goodTube_playbackSpeed + '&goodTube_hideInfoCards=' + goodTube_hideInfoCards + '&goodTube_hideEndScreen=' + goodTube_hideEndScreen + startTimeParam, '*');
// Indicate we've completed the first load
goodTube_firstLoad = false;
}
// Otherwise, for all other loads
else {
// Load the video via the iframe api
goodTube_player.contentWindow.postMessage('goodTube_load_' + goodTube_getParams['v'] + '|||' + startTime + '|||' + playlist, '*');
}
// Sync the starting time
if (goodTube_alwaysStart === 'false') {
goodTube_player_syncStartingTime();
}
// Play the video (this solves some edge cases in Firefox)
if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
goodTube_player_play();
}
}
// Sync the starting time
let goodTube_player_syncStartingTime_timeout = setTimeout(() => {}, 0);
function goodTube_player_syncStartingTime() {
// Make sure the player iframe has loaded
if (!goodTube_playerIframeLoaded) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_syncStartingTime_timeout);
// Create a new timeout to try again
goodTube_player_syncStartingTime_timeout = setTimeout(goodTube_player_syncStartingTime, 100);
// Don't do anything else
return;
}
// Re fetch the page API
goodTube_page_api = document.getElementById('movie_player');
// Get the video data to check loading state and video id
let videoData = false;
let videoId = false;
if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function' && typeof goodTube_page_api.getCurrentTime === 'function') {
videoData = goodTube_page_api.getVideoData();
videoId = videoData.video_id;
}
// If there's no video data, no video id, or the id doesn't match the one in the query params yet (it hasn't loaded)
if (!videoData || !videoId || videoId !== goodTube_getParams['v']) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_syncStartingTime_timeout);
// Create a new timeout to try again
goodTube_player_syncStartingTime_timeout = setTimeout(goodTube_player_syncStartingTime, 100);
// Don't do anything else
return;
}
// Setup the sync time
let syncTime = Math.floor(goodTube_page_api.getCurrentTime());
// If the sync time is greater than or equal to 10s (this accounts for some delayed loading time)
if (syncTime >= 10) {
// Sync our player
goodTube_player_skipTo(syncTime, videoId);
}
}
// Clear and hide the player
function goodTube_player_clear(fallbackActive = false) {
// If we're not in picture in picture mode
if (!goodTube_pip) {
// Clear the "hide and mute ads" fallback
if (fallbackActive) {
// Refetch the page api
goodTube_page_api = document.getElementById('movie_player');
// Make sure the API is all good
if (goodTube_page_api && typeof goodTube_page_api.pauseVideo === 'function' && typeof goodTube_page_api.mute === 'function') {
// Pause and mute the video (we don't use "stopVideo" because this messes with age restricted videos, triggers some weird detection)
goodTube_page_api.pauseVideo();
goodTube_page_api.mute();
}
}
// Clear the regular player
else {
// Stop the video via the iframe api
goodTube_player.contentWindow.postMessage('goodTube_stopVideo', '*');
}
}
// Hide the player
goodTube_helper_hideElement(goodTube_playerWrapper);
}
// Skip to time
function goodTube_player_skipTo(time, videoId = '') {
goodTube_player.contentWindow.postMessage('goodTube_skipTo_' + time + '|||' + videoId, '*');
}
// Pause
function goodTube_player_pause() {
goodTube_player.contentWindow.postMessage('goodTube_pause', '*');
}
// Play
let goodTube_player_play_timeout = setTimeout(() => {}, 0);
function goodTube_player_play() {
// If the tab isn't in focus OR our player hasn't loaded
if (!goodTube_tabInFocus || !goodTube_playerIframeLoaded) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_play_timeout);
// Create a new timeout to try again
goodTube_player_play_timeout = setTimeout(goodTube_player_play, 100);
// Don't do anything else
return;
}
// If the "hide and mute ads" fallback is disabled
if (!goodTube_fallback) {
goodTube_player.contentWindow.postMessage('goodTube_play|||' + goodTube_getParams['v'], '*');
}
// Otherwise, the "hide and mute ads" fallback is enabled
else {
// Re-fetch the page api
goodTube_page_api = document.getElementById('movie_player');
// Get the video data
let videoData = false;
if (goodTube_page_api && typeof goodTube_page_api.getVideoData === 'function') {
videoData = goodTube_page_api.getVideoData();
}
// If the correct video hasn't loaded yet (based on the ID in the query params)
if (!videoData || goodTube_getParams['v'] !== videoData.video_id) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_player_play_timeout);
// Create a new timeout to try again
goodTube_player_play_timeout = setTimeout(goodTube_player_play, 100);
// Don't do anything else
return;
}
// Make sure the video has not ended (this solves an edge case)
let videoElement = document.querySelector('#movie_player video');
if (videoElement) {
if (videoElement.currentTime >= videoElement.duration) {
return;
}
}
// Play the video
if (goodTube_page_api && typeof goodTube_page_api.playVideo === 'function') {
// Wait 100ms (this solves an edge case)
setTimeout(() => {
// Force the video to play
goodTube_page_api.playVideo();
}, 100);
}
}
}
/* Keyboard shortcuts
------------------------------------------------------------------------------------------ */
// Add keyboard shortcuts
function goodTube_shortcuts_init() {
// Add event listeners
document.addEventListener('keydown', goodTube_shortcuts_keypress, true);
document.addEventListener('keyup', goodTube_shortcuts_keypress, true);
}
// Define the keypress function for the event listeners
function goodTube_shortcuts_keypress(event) {
// If we're not watching a video OR the "hide and mute ads" fallback is active
if (!goodTube_helper_watchingVideo() || goodTube_fallback) {
// Don't do anything
return;
}
// Define the shortcuts we allow
let allowedShortcuts = [
// Playback speed
{
key: '>',
code: false,
ctrl: false,
shift: true,
alt: false
},
{
key: '<',
code: false,
ctrl: false,
shift: true,
alt: false
},
// Skip frame
{
key: ',',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '.',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Skip 5 seconds
{
key: 'arrowleft',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'arrowright',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Skip 10 seconds
{
key: 'j',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'l',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Play / pause
{
key: ' ',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'k',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'mediaplaypause',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Mute
{
key: 'm',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Fullscreen
{
key: 'f',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Captions
{
key: 'c',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Text opacity
{
key: 'o',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Window opacity
{
key: 'w',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Font size
{
key: '=',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '-',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Panning (spherical videos)
{
key: 'w',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'a',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 's',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: 'd',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Zooming (spherical videos)
{
key: '[',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: ']',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: false,
code: 'numpadadd',
ctrl: false,
shift: false,
alt: false
},
{
key: false,
code: 'numpadsubtract',
ctrl: false,
shift: false,
alt: false
},
// Skip to percentage
{
key: '0',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '1',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '2',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '3',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '4',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '5',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '6',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '7',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '8',
code: false,
ctrl: false,
shift: false,
alt: false
},
{
key: '9',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Picture in picture
{
key: 'i',
code: false,
ctrl: false,
shift: false,
alt: false
},
// Seek chapters
{
key: 'arrowleft',
code: false,
ctrl: true,
shift: false,
alt: false
},
{
key: 'arrowright',
code: false,
ctrl: true,
shift: false,
alt: false
}
];
// Which key was pressed?
let keyPressed = event.key.toLowerCase();
let codePressed = event.code.toLowerCase();
// Is ctrl or meta (mac) pressed?
let ctrlPressed = event.ctrlKey;
if (event.metaKey) {
ctrlPressed = event.metaKey;
}
// Is shift pressed?
let shiftPressed = event.shiftKey;
// Is alt pressed?
let altPressed = event.altKey;
// Ensure we've pressed an allowed shortcut
let allowKeypress = false;
allowedShortcuts.forEach((allowedShortcut) => {
if (
(keyPressed === allowedShortcut.key || codePressed === allowedShortcut.code) &&
ctrlPressed === allowedShortcut.ctrl &&
shiftPressed === allowedShortcut.shift &&
altPressed === allowedShortcut.alt
) {
allowKeypress = true;
}
});
// If we've allowed this keypress (because the shortcut was valid)
if (allowKeypress) {
// Get the currently focused element
let focusedElement = event.srcElement;
let focusedElement_tag = false;
let focusedElement_id = false;
if (focusedElement) {
if (typeof focusedElement.nodeName !== 'undefined') {
focusedElement_tag = focusedElement.nodeName.toLowerCase();
}
if (typeof focusedElement.getAttribute !== 'undefined') {
focusedElement_id = focusedElement.getAttribute('id');
}
}
// If we're not focused on a HTML form element
if (
!focusedElement ||
(
focusedElement_tag.indexOf('input') === -1 &&
focusedElement_tag.indexOf('label') === -1 &&
focusedElement_tag.indexOf('select') === -1 &&
focusedElement_tag.indexOf('textarea') === -1 &&
focusedElement_tag.indexOf('fieldset') === -1 &&
focusedElement_tag.indexOf('legend') === -1 &&
focusedElement_tag.indexOf('datalist') === -1 &&
focusedElement_tag.indexOf('output') === -1 &&
focusedElement_tag.indexOf('option') === -1 &&
focusedElement_tag.indexOf('optgroup') === -1 &&
focusedElement_id !== 'contenteditable-root'
)
) {
// Prevent default actions
event.preventDefault();
event.stopImmediatePropagation();
// Swap media key to spacebar when we pass it down. This ensures that the play / pause works correctly
let event_key = event.key;
let event_keyCode = event.keyCode;
if (keyPressed === 'mediaplaypause') {
event_key = ' ';
event_keyCode = 32;
}
// Pass the keyboard shortcut to the iframe
goodTube_player.contentWindow.postMessage('goodTube_shortcut_' + event.type + '_' + event_key + '_' + event_keyCode + '_' + event.ctrlKey + '_' + event.metaKey + '_' + event.shiftKey + '_' + event.altKey, '*');
}
}
}
/* Navigation
------------------------------------------------------------------------------------------ */
// Play the next video
function goodTube_nav_next() {
// // Re fetch the page API
// goodTube_page_api = document.getElementById('movie_player');
// // Make sure it exists
// if (goodTube_page_api && typeof goodTube_page_api.nextVideo === 'function') {
// // Play the previous video
// goodTube_page_api.nextVideo();
// }
// Let's try this, see if we can evade detection better
goodTube_helper_shortcut('next');
// Debug message
console.log('[GoodTube] Playing next video...');
}
// Play the previous video
function goodTube_nav_prev() {
// // Re fetch the page API
// goodTube_page_api = document.getElementById('movie_player');
// // Make sure it exists
// if (goodTube_page_api && typeof goodTube_page_api.previousVideo === 'function') {
// // Play the previous video
// goodTube_page_api.previousVideo();
// }
// Let's try this, see if we can evade detection better
goodTube_helper_shortcut('previous');
// Debug message
console.log('[GoodTube] Playing previous video...');
}
// Video has ended
let goodTube_nav_videoEnded_timeout = setTimeout(() => {}, 0);
function goodTube_nav_videoEnded() {
// If autoplay is enabled
if (goodTube_autoplay === 'true') {
// Play the next video
goodTube_nav_next();
}
// Otherwise, if we're viewing a playliust
else if (goodTube_playlist) {
// Populate the playlist info
goodTube_player_populatePlaylistInfo();
// Make sure the playlist info exists
if (!goodTube_playlist || !goodTube_playlistIndex) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_nav_videoEnded_timeout);
// Try again
goodTube_nav_videoEnded_timeout = setTimeout(goodTube_nav_videoEnded, 100);
// Don't do anything else
return;
}
// If we're not on the last video
if (goodTube_playlistIndex < (goodTube_playlist.length - 1)) {
// Play the next video
goodTube_nav_next();
}
}
}
// Show or hide the end screen (based on autoplay, not the setting)
function goodTube_nav_showHideEndScreen() {
// Re fetch the page API
goodTube_page_api = document.getElementById('movie_player');
// Show the end screen
let hideEndScreen = false;
// If autoplay is on, hide the end screen
if (goodTube_autoplay === 'true') {
hideEndScreen = true;
}
// Otherwise, if we're viewing a playlist
else if (goodTube_playlist) {
// Hide the end screen
hideEndScreen = true;
// If we're on the last video
if (goodTube_playlistIndex === (goodTube_playlist.length - 1)) {
// Show the end screen
hideEndScreen = false;
}
}
// Hide the end screen
if (hideEndScreen) {
goodTube_player.contentWindow.postMessage('goodTube_endScreen_hide', '*');
}
// Otherwise show the end screen
else {
goodTube_player.contentWindow.postMessage('goodTube_endScreen_show', '*');
}
}
/* Usage stats
------------------------------------------------------------------------------------------
Don't worry everyone - these are just simple counters that let me know the following;
- Daily unique users
- Total unique users
- Daily videos played
- Total videos played
This is only in here so I can have some fun and see how many people use this thing I made **no private info is tracked**
*/
// Count unique users
function goodTube_stats_user() {
/* Get today's date as yyyy-mm-dd (UTC time)
-------------------------------------------------- */
let date_local = new Date();
let date_utc = Date.UTC(date_local.getUTCFullYear(), date_local.getUTCMonth(), date_local.getUTCDate(), date_local.getUTCHours(), date_local.getUTCMinutes(), date_local.getUTCSeconds());
let date_utc_formatted = new Date(date_utc);
let date_string = date_utc_formatted.toISOString().split('T')[0];
/* Daily unique users
-------------------------------------------------- */
// If there's no cookie
if (!goodTube_helper_getCookie('goodTube_uniqueUserStat_' + date_string)) {
// Count
fetch(
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x75\x73\x65\x72\x73\x5f\x64\x61\x69\x6c\x79\x2e\x70\x68\x70',
{
referrerPolicy: 'no-referrer'
}
);
// Set a cookie (2 days exp time - to limit the cookies we create)
goodTube_helper_setCookie('goodTube_uniqueUserStat_' + date_string, 'true', 2);
}
/* Total unique users
-------------------------------------------------- */
// If there's no cookie
if (!goodTube_helper_getCookie('goodTube_uniqueUserStat')) {
// Count
fetch(
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x75\x73\x65\x72\x73\x5f\x74\x6f\x74\x61\x6c\x2e\x70\x68\x70',
{
referrerPolicy: 'no-referrer'
}
);
// Set a cookie
goodTube_helper_setCookie('goodTube_uniqueUserStat', 'true');
}
}
// Count videos played
function goodTube_stats_video() {
/* Videos played (combined total and daily)
-------------------------------------------------- */
// Count
fetch(
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x76\x69\x64\x65\x6f\x73\x2e\x70\x68\x70',
{
referrerPolicy: 'no-referrer'
}
);
}
/* Core functions
------------------------------------------------------------------------------------------ */
// Init
let goodTube_initiated = false;
let goodTube_init_timeout = setTimeout(() => {}, 0);
function goodTube_init(retrying = false) {
// If we're not retrying
if (!retrying) {
// Debug message
if (window.top === window.self) {
console.log('\n==================================================\n ______ ________ __\n / ____/___ ____ ____/ /_ __/_ __/ /_ ___\n / / __/ __ \\/ __ \\/ __ / / / / / / / __ \\/ _ \\\n / /_/ / /_/ / /_/ / /_/ / / / / /_/ / /_/ / __/\n \\____/\\____/\\____/\\____/ /_/ \\____/_____/\\___/\n\n==================================================');
console.log('[GoodTube] Initiating...');
}
// Listen for messages from the iframes
window.addEventListener('message', goodTube_receiveMessage);
// Mute and pause all Youtube videos
goodTube_youtube_pauseMuteVideos();
// Init the rest once the DOM is ready
document.addEventListener('DOMContentLoaded', goodTube_init_domReady);
// Also check if the DOM is already loaded, as if it is, the above event listener will not trigger
if (document.readyState === 'interactive' || document.readyState === 'complete') {
goodTube_init_domReady();
}
}
// And try this to check if the DOM is ready, seems to be the only reliable method in all browsers (which is insane, I know...thanks Safari)
if (!document.body || !document.head) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_init_timeout);
// Create a new timeout
goodTube_init_timeout = setTimeout(() => { goodTube_init(true); }, 1);
}
// Otherwise, the DOM is ready
else {
goodTube_init_domReady();
}
}
// Init when DOM is ready
function goodTube_init_domReady() {
// Sanity check - only do this once (this fixes known load issues in Firefox)
if (goodTube_initiated) {
return;
}
goodTube_initiated = true;
// Check the tab focus state
goodTube_checkTabFocus();
// Add a CSS class to show or hide elements
goodTube_helper_showHide_init();
// Hide page elements
goodTube_youtube_hidePageElements();
// Init our player
goodTube_player_init();
// Init the "hide and mute ads" fallback
goodTube_hideAndMuteAdsFallback_init();
// Usage stats
goodTube_stats_user();
// Keyboard shortcuts
goodTube_shortcuts_init();
// Init the menu
goodTube_menu();
}
// Listen for messages from the iframe
let goodTube_receiveMessage_timeout = setTimeout(() => {}, 0);
function goodTube_receiveMessage(event) {
// Make sure some data exists
if (typeof event.data !== 'string') {
return;
}
// Make sure the DOM is ready, if not retry (this ensures that the message will fire eventually)
// Use this method to check if the DOM is ready, seems to be the only reliable method in all browsers (which is insane, I know...thanks Safari)
if (!document.body || !document.head) {
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_receiveMessage_timeout);
// Create a new timeout
goodTube_receiveMessage_timeout = setTimeout(() => { goodTube_receiveMessage(event); }, 100);
}
// Proxy iframe has loaded
else if (event.data === 'goodTube_proxyIframe_loaded') {
goodTube_proxyIframeLoaded = true;
}
// Player iframe has loaded
else if (event.data === 'goodTube_playerIframe_loaded') {
goodTube_playerIframeLoaded = true;
// Show the player iframe
goodTube_player.style.display = 'block';
}
// Picture in picture
if (event.data.indexOf('goodTube_pip_') !== -1) {
let pipEnabled = event.data.replace('goodTube_pip_', '');
if (pipEnabled === 'true') {
goodTube_pip = true;
}
else {
goodTube_pip = false;
// If we're not viewing a video
if (typeof goodTube_getParams['v'] === 'undefined') {
// Clear the player
goodTube_player_clear();
}
}
}
// Save the playback speed as a cookie
else if (event.data.indexOf('goodTube_playbackSpeed_') !== -1) {
goodTube_helper_setCookie('goodTube_playbackSpeed', event.data.replace('goodTube_playbackSpeed_', ''));
goodTube_playbackSpeed = event.data.replace('goodTube_playbackSpeed_', '');
}
// Previous video
else if (event.data === 'goodTube_prevVideo') {
goodTube_nav_prev();
}
// Next video
else if (event.data === 'goodTube_nextVideo') {
goodTube_nav_next();
}
// Video has ended
else if (event.data === 'goodTube_videoEnded') {
goodTube_nav_videoEnded();
}
// Theater mode (toggle) - this should only work when not in fullscreen
else if (event.data === 'goodTube_theater' && !document.fullscreenElement) {
// Find the theater button
let theaterButton = document.querySelector('.ytp-size-button');
// If we found the theater button
if (theaterButton) {
// Click it
goodTube_helper_click(theaterButton);
}
}
// Autoplay
else if (event.data === 'goodTube_autoplay_false') {
goodTube_helper_setCookie('goodTube_autoplay', 'false');
goodTube_autoplay = 'false';
}
else if (event.data === 'goodTube_autoplay_true') {
goodTube_helper_setCookie('goodTube_autoplay', 'true');
goodTube_autoplay = 'true';
}
// Sync main player (only if we're viewing a video page AND the "hide and mute ads" fallback is inactive)
else if (event.data.indexOf('goodTube_syncMainPlayer_') !== -1 && goodTube_helper_watchingVideo() && !goodTube_fallback) {
// Parse the data
let syncTime = parseFloat(event.data.replace('goodTube_syncMainPlayer_', ''));
// Target the youtube video element
let youtubeVideoElement = document.querySelector('#movie_player video');
// Re-fetch the page API
goodTube_page_api = document.getElementById('movie_player');
// Make sure the API is all good
if (!goodTube_page_api || typeof goodTube_page_api.seekTo !== 'function' || typeof goodTube_page_api.playVideo !== 'function' || typeof goodTube_page_api.mute !== 'function' || typeof goodTube_page_api.setVolume !== 'function') {
return;
}
// If we found the video element
// AND we've not already synced to this point (this stops it continuing to sync when ended for no reason, we also need to round it down as it seems to be unreliable)
// AND ads are not showing (we don't want to touch the the time when ads are playing, this triggers detection)
if (youtubeVideoElement && Math.floor(youtubeVideoElement.currentTime) !== Math.floor(syncTime) && !goodTube_helper_adsShowing()) {
// Set a variable to indicate we're syncing the player (this stops the automatic pausing of all videos)
goodTube_syncingPlayer = true;
// Play the video via the page API (this is the only reliable way)
goodTube_page_api.playVideo();
// Sync the current time using the page API - 500ms (this is the only reliable way)
goodTube_page_api.seekTo((syncTime - .5));
// Then mute the video via the page API (this helps to prevent audio flashes)
goodTube_page_api.mute();
goodTube_page_api.setVolume(0);
// Then mute the video via HTML (playing it unmutes it for some reason)
youtubeVideoElement.volume = 0;
youtubeVideoElement.muted = true;
// Clear timeout first to solve memory leak issues
clearTimeout(goodTube_receiveMessage_timeout);
// After 1000ms stop syncing (and let the pause actions handle the pausing)
goodTube_receiveMessage_timeout = setTimeout(() => {
goodTube_syncingPlayer = false;
}, 1000);
}
}
// Enable "hide and mute ads" fallback
else if (event.data === 'goodTube_fallback_enable') {
goodTube_fallback = true;
// Add a class to the
if (document.body && !document.body.classList.contains('goodTube_fallback')) {
document.body.classList.add('goodTube_fallback');
}
// Unset the aspect ratio
goodTube_youtube_unsetAspectRatio();
// Sync the autoplay
goodTube_hideAndMuteAdsFallback_syncAutoplay();
// Play the video (this solves some edge cases in Firefox)
if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
goodTube_player_play();
}
// If we're in fullscreen already
if (document.fullscreenElement) {
// Exit fullscreen
document.exitFullscreen();
// Fullscreen the normal Youtube player (wait 100ms, this delay is required because browsers animate fullscreen animations and we can't change this)
window.setTimeout(() => {
let fullscreenButton = document.querySelector('.ytp-fullscreen-button');
if (fullscreenButton) {
goodTube_helper_click(fullscreenButton);
}
}, 100);
}
}
// Disable "hide and mute ads" fallback
else if (event.data === 'goodTube_fallback_disable') {
goodTube_fallback = false;
// Remove the class from the
if (document.body && document.body.classList.contains('goodTube_fallback')) {
document.body.classList.remove('goodTube_fallback');
}
// If we're in fullscreen already
if (document.fullscreenElement) {
// Exit fullscreen
document.exitFullscreen();
// Fullscreen the normal Youtube player (wait 100ms, this delay is required because browsers animate fullscreen animations and we can't change this)
window.setTimeout(() => {
goodTube_player.contentWindow.postMessage('goodTube_fullscreen', '*');
}, 100);
}
}
// Sync the aspect ratio
else if (event.data.indexOf('goodTube_syncAspectRatio_') !== -1) {
// If you're viewing a short
if (window.location.href.indexOf('/shorts') !== -1) {
// Remove the aspect ratio changes
goodTube_youtube_unsetAspectRatio();
}
// Otherwise, for all other videos
else {
// Set the aspect ratio
let aspectRatio = event.data.replace('goodTube_syncAspectRatio_', '').split('_');
goodTube_youtube_setAspectRatio(aspectRatio[0], aspectRatio[1]);
}
}
// Cancel any pending play actions
else if (event.data === 'goodTube_cancelPlay') {
// Clear the re-try timeout for the "goodTube_player_play" function
clearTimeout(goodTube_player_play_timeout);
}
}
// Actions
function goodTube_actions() {
// Get the previous and current URL
// Remove hashes, these mess with things sometimes
// Also remove "index="
let previousUrl = goodTube_previousUrl;
if (previousUrl) {
previousUrl = previousUrl.split('#')[0];
previousUrl = previousUrl.split('index=')[0];
}
let currentUrl = window.location.href;
if (currentUrl) {
currentUrl = currentUrl.split('#')[0];
currentUrl = currentUrl.split('index=')[0];
}
// If the URL has changed (this will always fire on first page load)
if (previousUrl !== currentUrl) {
// The URL has changed, so setup our player
// ----------------------------------------------------------------------------------------------------
// Setup GET parameters
goodTube_getParams = goodTube_helper_setupGetParams();
// If we're viewing a video
if (goodTube_helper_watchingVideo()) {
// Load the video
goodTube_player_load();
// Usage stats
goodTube_stats_video();
}
// Otherwise if we're not viewing a video
else {
// Clear the player
goodTube_player_clear();
}
// Set the previous URL (which pauses this function until the URL changes again)
goodTube_previousUrl = window.location.href;
}
// If we're viewing a video
if (goodTube_helper_watchingVideo()) {
// Show or hide the end screen (based on autoplay, not the setting)
goodTube_nav_showHideEndScreen();
// Support timestamp links
goodTube_youtube_timestampLinks();
// If the "hide and mute ads" fallback is inactive
if (!goodTube_fallback) {
// Turn off autoplay
goodTube_youtube_turnOffAutoplay();
}
// Remove the "are you still watching" popup
goodTube_youtube_removeAreYouStillWatchingPopup();
// Position and size the player
goodTube_player_positionAndSize();
// Check to enable or disable the "hide and mute ads" fallback overlay
goodTube_hideAndMuteAdsFallback_check();
}
// If we're not watching a video
else {
// Stop the video (this solves some weird edge case where the video can be playing in the background)
goodTube_player.contentWindow.postMessage('goodTube_stopVideo', '*');
}
// Hide shorts (real time)
goodTube_youtube_hideShortsRealTime();
// Hide members only videos if they're not enabled
if (goodTube_hideMembersOnlyVideos === 'true') {
goodTube_youtube_hideMembersOnlyVideos();
}
}
// Init menu
function goodTube_menu() {
// Create the menu container
let menuContainer = document.createElement('div');
// Add the menu container to the page
document.body.appendChild(menuContainer);
// Configure the settings to show their actual values
let shortsEnabled = ' checked';
if (goodTube_shorts === 'true') {
shortsEnabled = '';
}
let hideInfoCards = '';
if (goodTube_hideInfoCards === 'true') {
hideInfoCards = ' checked';
}
let hideEndScreen = '';
if (goodTube_hideEndScreen === 'true') {
hideEndScreen = ' checked';
}
let hideSuggestedVideos = '';
if (goodTube_hideSuggestedVideos === 'true') {
hideSuggestedVideos = ' checked';
}
let hideComments = '';
if (goodTube_hideComments === 'true') {
hideComments = ' checked';
}
let hideAiSummaries = '';
if (goodTube_hideAiSummaries === 'true') {
hideAiSummaries = ' checked';
}
let hideMembersOnlyVideos = '';
if (goodTube_hideMembersOnlyVideos === 'true') {
hideMembersOnlyVideos = ' checked';
}
let alwaysStart = '';
if (goodTube_alwaysStart === 'true') {
alwaysStart = ' checked';
}
let blackBackground = '';
if (goodTube_blackBackground === 'true') {
blackBackground = ' checked';
}
let videosPerRow_default = '';
let videosPerRow_2 = '';
let videosPerRow_3 = '';
let videosPerRow_4 = '';
let videosPerRow_5 = '';
let videosPerRow_6 = '';
let videosPerRow_7 = '';
let videosPerRow_8 = '';
if (videosPerRow_default === 'default') {
videosPerRow_default = ' selected';
}
else if (goodTube_videosPerRow === '2') {
videosPerRow_2 = ' selected';
}
else if (goodTube_videosPerRow === '3') {
videosPerRow_3 = ' selected';
}
else if (goodTube_videosPerRow === '4') {
videosPerRow_4 = ' selected';
}
else if (goodTube_videosPerRow === '5') {
videosPerRow_5 = ' selected';
}
else if (goodTube_videosPerRow === '6') {
videosPerRow_6 = ' selected';
}
else if (goodTube_videosPerRow === '7') {
videosPerRow_7 = ' selected';
}
else if (goodTube_videosPerRow === '8') {
videosPerRow_8 = ' selected';
}
// Add content to the menu container
goodTube_helper_innerHTML(menuContainer, `
✖
This adblocker is 100% free to use and always will be.
It has helped over 175,000 people remove the unbearable ads from Youtube.
This project has been made entirely by myself, as just one developer. Countless hours and late nights have gone into making this and I continue to work on updating and maintaining the project regularly. I remain dedicated to ensuring this solution continues to work for everyone (despite Youtube's best efforts to stop adblockers).
Donations help to keep this project going and support the wider community who use it. If you would like to say thank you and can give something back, it would be greatly appreciated.
Update - Seriously everyone, I am flat broke - so small donations help a lot. If everyone donated just $1 I could finally go travelling with my partner rather than just living week to week. 🙏🏼
How can I share this with friends?
You can send them this link. It has all of the install instructions.
Do I need to manually update this?
Nope, updates are pushed to you automatically so you don't have to do anything to use the latest version.
Playlists skip to the next video every few seconds
This is usually caused by another adblocker which Youtube is detecting. To fix this problem, first disable all of your other adblockers (for Youtube only, you can leave them on for other websites). Then clear your cookies and cache (this is important). Once that's done, refresh Youtube and the problem should be fixed.
I can't use the miniplayer
The Youtube miniplayer is not supported. Instead this uses "Picture in Picture" mode, which is the new standard for the web. Unfortunately Firefox does not support the Picture in Picture API, so the button is disabled in Firefox until they decide to include this feature.
Is this compatible with other Youtube extensions?
Short answer - probably not. This heavily modifies how Youtube works in order to block ads. A key part of this is replacing the default Youtube player with their "embedded" player. This means that unless your extension also works for embedded Youtube videos (like where you view a Youtube video on another website), it generally won't be compatible. Unfortunately there's not much I can do to support these extensions as a result. Honestly though - you probably never needed them anyway, just play the video and be happy.
I'm having a different problem
If you're having a different issue, most of the time you will find it's caused by a conflicting extension you have installed. The first thing to do is turn off all other extensions you have installed. Leave only Tampermonkey and GoodTube enabled. Then refresh Youtube, check if the problem is fixed. If it is, then you know one of them is causing the issue. Turn your other extensions back on back on one at a time until you find the problem.
Report an issue
Your message has been sent successfully.
`);
// Style the menu
let style = document.createElement('style');
style.textContent = `
/* Menu button
---------------------------------------------------------------------------------------------------- */
.goodTube_menuButton {
display: block;
position: fixed;
bottom: 16px;
right: 16px;
background: #0f0f0f;
border-radius: 9999px;
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
width: 48px;
height: 48px;
z-index: 999;
transition: background .2s linear, opacity .2s linear, box-shadow .2s linear;
opacity: 1;
cursor: pointer;
}
.goodTube_menuButton img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
width: 26px;
}
.goodTube_menuButton::before {
content: 'Settings';
background: rgba(0, 0, 0, .9);
border-radius: 4px;
color: #ffffff;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 8px;
padding-right: 8px;
position: absolute;
left: 50%;
top: -26px;
transform: translateX(-50%);
letter-spacing: 0.04em;
opacity: 0;
transition: opacity .2s ease-in-out, top .2s ease-in-out;
pointer-events: none;
text-decoration: none;
}
.goodTube_menuButton::after {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid rgba(0, 0, 0, .9);
opacity: 0;
transition: opacity .2s ease-in-out, top .2s ease-in-out;
pointer-events: none;
text-decoration: none;
}
.goodTube_menuButton:hover {
background: #252525;
box-shadow: 0 0 12px rgba(0, 0, 0, .5);
}
.goodTube_menuButton:hover::before,
.goodTube_menuButton:hover::after {
opacity: 1;
}
.goodTube_menuButton:hover::before {
top: -29px;
}
.goodTube_menuButton:hover::after {
top: -9px;
}
.goodTube_menuClose {
display: block;
position: fixed;
bottom: 51px;
right: 16px;
width: 14px;
height: 14px;
background: #ffffff;
color: #000000;
font-size: 9px;
font-weight: 700;
border-radius: 999px;
text-align: center;
line-height: 13px;
z-index: 9999;
box-shadow: 0 0 4px rgba(0, 0, 0, .5);
opacity: 1;
text-decoration: none;
cursor: pointer;
}
/* Modal container
---------------------------------------------------------------------------------------------------- */
.goodTube_modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
transition: opacity .2s linear;
pointer-events: none;
backface-visibility: hidden;
min-width: 320px;
}
.goodTube_modal:not(.visible) .goodTube_button {
pointer-events: none;
}
.goodTube_modal.visible {
pointer-events: all;
opacity: 1;
}
.goodTube_modal.visible .goodTube_button {
pointer-events: all;
}
.goodTube_modal * {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.goodTube_modal .goodTube_modal_overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background: rgba(0,0,0,.8);
}
.goodTube_modal .goodTube_modal_inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(round(-50%, 1px), round(-50%, 1px));
width: 780px;
max-width: calc(100% - 32px);
max-height: calc(100% - 32px);
z-index: 2;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 0 24px rgba(0, 0, 0, .5);
font-family: Roboto, Arial, sans-serif;
padding: 24px;
overflow: auto;
}
.goodTube_modal .goodTube_modal_inner .goodTube_modal_closeButton {
position: absolute;
top: 17px;
right: 12px;
color: #333;
font-size: 20px;
font-weight: 400;
text-decoration: none;
width: 40px;
height: 40px;
background: #ffffff;
border-radius: 9999px;
text-align: center;
line-height: 40px;
transition: background .2s linear;
cursor: pointer;
}
.goodTube_modal .goodTube_modal_inner .goodTube_modal_closeButton:hover {
background: #dddddd;
}
/* Modal inner
---------------------------------------------------------------------------------------------------- */
.goodTube_modal .goodTube_title {
font-weight: 700;
font-size: 22px;
padding-bottom: 16px;
}
.goodTube_modal .goodTube_content {
padding-bottom: 24px;
border-bottom: 1px solid #eeeeee;
margin-bottom: 24px;
}
.goodTube_modal .goodTube_content:last-child {
border-bottom: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.goodTube_modal .goodTube_content .goodTube_setting {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.goodTube_modal .goodTube_content .goodTube_setting input {
width: 24px;
height: 24px;
min-width: 24px;
min-height: 24px;
border-radius: 4px;
border: 1px solid #333;
overflow: hidden;
cursor: pointer;
}
.goodTube_modal .goodTube_content .goodTube_setting select {
border-radius: 4px;
border: 1px solid #999;
width: 100%;
font-size: 14px;
color: #000000;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 8px;
padding-right: 16px;
font-family: Roboto, Arial, sans-serif;
transition: border .2s linear;
width: 96px;
min-width: 96px;
font-weight: 400;
}
.goodTube_modal .goodTube_content .goodTube_setting select {
border: 1px solid #333;
}
.goodTube_modal .goodTube_content .goodTube_setting label {
font-size: 15px;
color: #000000;
font-weight: 500;
cursor: pointer;
}
.goodTube_modal .goodTube_button {
all: initial;
margin: 0;
padding: 0;
box-sizing: border-box;
display: inline-block;
background: #e84a82;
color: #ffffff;
text-align: center;
font-size: 15px;
font-weight: 700;
padding-top: 12px;
padding-bottom: 12px;
padding-left: 18px;
padding-right: 18px;
letter-spacing: 0.024em;
border-radius: 4px;
font-family: Roboto, Arial, sans-serif;
cursor: pointer;
transition: background .2s linear;
}
.goodTube_modal .goodTube_button:hover {
background: #fa5b93;
}
.goodTube_modal .goodTube_heart {
color: #e01b6a;
font-size: 24px;
}
.goodTube_modal .goodTube_text {
display: block;
font-size: 15px;
padding-bottom: 16px;
line-height: 130%;
}
.goodTube_modal .goodTube_text:last-child {
padding-bottom: 0;
}
.goodTube_modal .goodTube_text a {
color: #e84a82;
text-decoration: underline;
}
.goodTube_modal .goodTube_report {
}
.goodTube_modal .goodTube_successText {
font-size: 15px;
padding-bottom: 16px;
line-height: 130%;
display: none;
}
.goodTube_modal .goodTube_report input:not(.goodTube_button),
.goodTube_modal .goodTube_report textarea {
border-radius: 4px;
border: 1px solid #999;
width: 100%;
font-size: 14px;
color: #000000;
padding-top: 12px;
padding-bottom: 12px;
padding-left: 16px;
padding-right: 16px;
font-family: Roboto, Arial, sans-serif;
transition: border .2s linear;
}
.goodTube_modal .goodTube_report input:not(.goodTube_button)::placeholder,
.goodTube_modal .goodTube_report textarea::placeholder {
color: #666666;
}
.goodTube_modal .goodTube_report input:not(.goodTube_button):focus,
.goodTube_modal .goodTube_report textarea:focus {
border: 1px solid #333;
}
.goodTube_modal .goodTube_report input:not(.goodTube_button) {
margin-bottom: 12px;
}
.goodTube_modal .goodTube_report textarea {
margin-bottom: 16px;
height: 128px;
}
`;
document.head.appendChild(style);
/* Menu button
-------------------------------------------------- */
// Target the elements
let menuButton = document.querySelector('.goodTube_menuButton');
let menuClose = document.querySelector('.goodTube_menuClose');
// Support the close button
if (menuClose) {
menuClose.addEventListener('click', () => {
menuButton.remove();
menuClose.remove();
});
}
/* Modal
-------------------------------------------------- */
// Target the elements
let modal = document.querySelector('.goodTube_modal');
let modalOverlay = document.querySelector('.goodTube_modal .goodTube_modal_overlay');
let modalCloseButton = document.querySelector('.goodTube_modal .goodTube_modal_closeButton');
// Open the modal
if (menuButton) {
menuButton.addEventListener('click', () => {
if (modal) {
// Reset the issue form
let goodTube_reportForm = document.querySelector('.goodTube_report');
if (goodTube_reportForm) {
goodTube_reportForm.style.display = 'block';
}
let goodTube_reportSuccessText = document.querySelector('.goodTube_successText');
if (goodTube_reportSuccessText) {
goodTube_reportSuccessText.style.display = 'none';
}
let goodTube_reportEmail = document.querySelector('.goodTube_reportEmail');
if (goodTube_reportEmail) {
goodTube_reportEmail.value = '';
}
let goodTube_reportText = document.querySelector('.goodTube_reportText');
if (goodTube_reportText) {
goodTube_reportText.value = '';
}
// Show the modal
modal.classList.add('visible');
}
});
}
// Close the modal
if (modalOverlay) {
modalOverlay.addEventListener('click', () => {
if (modal && modal.classList.contains('visible')) {
modal.classList.remove('visible');
}
});
}
if (modalCloseButton) {
modalCloseButton.addEventListener('click', () => {
if (modal && modal.classList.contains('visible')) {
modal.classList.remove('visible');
}
});
}
document.addEventListener('keydown', (event) => {
if (event.key.toLowerCase() === 'escape') {
if (modal && modal.classList.contains('visible')) {
modal.classList.remove('visible');
}
}
});
/* Settings
-------------------------------------------------- */
let goodTube_button_saveSettings = document.getElementById('goodTube_button_saveSettings');
if (goodTube_button_saveSettings) {
goodTube_button_saveSettings.addEventListener('click', () => {
// Shorts
let goodTube_setting_shorts = document.querySelector('.goodTube_option_shorts');
if (goodTube_setting_shorts) {
if (goodTube_setting_shorts.checked) {
goodTube_helper_setCookie('goodTube_shorts', 'false');
}
else {
goodTube_helper_setCookie('goodTube_shorts', 'true');
}
}
// Hide info cards
let goodTube_setting_hideInfoCards = document.querySelector('.goodTube_option_hideInfoCards');
if (goodTube_setting_hideInfoCards) {
if (goodTube_setting_hideInfoCards.checked) {
goodTube_helper_setCookie('goodTube_hideInfoCards', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideInfoCards', 'false');
}
}
// Hide end screen
let goodTube_setting_hideEndScreen = document.querySelector('.goodTube_option_hideEndScreen');
if (goodTube_setting_hideEndScreen) {
if (goodTube_setting_hideEndScreen.checked) {
goodTube_helper_setCookie('goodTube_hideEndScreen', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideEndScreen', 'false');
}
}
// Hide suggested videos
let goodTube_setting_hideSuggestedVideos = document.querySelector('.goodTube_option_hideSuggestedVideos');
if (goodTube_setting_hideSuggestedVideos) {
if (goodTube_setting_hideSuggestedVideos.checked) {
goodTube_helper_setCookie('goodTube_hideSuggestedVideos', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideSuggestedVideos', 'false');
}
}
// Hide comments
let goodTube_setting_hideComments = document.querySelector('.goodTube_option_hideComments');
if (goodTube_setting_hideComments) {
if (goodTube_setting_hideComments.checked) {
goodTube_helper_setCookie('goodTube_hideComments', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideComments', 'false');
}
}
// Hide AI summaries
let goodTube_setting_hideAiSummaries = document.querySelector('.goodTube_option_hideAiSummaries');
if (goodTube_setting_hideAiSummaries) {
if (goodTube_setting_hideAiSummaries.checked) {
goodTube_helper_setCookie('goodTube_hideAiSummaries', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideAiSummaries', 'false');
}
}
// Hide members only videos
let goodTube_setting_hideMembersOnlyVideos = document.querySelector('.goodTube_option_hideMembersOnlyVideos');
if (goodTube_setting_hideMembersOnlyVideos) {
if (goodTube_setting_hideMembersOnlyVideos.checked) {
goodTube_helper_setCookie('goodTube_hideMembersOnlyVideos', 'true');
}
else {
goodTube_helper_setCookie('goodTube_hideMembersOnlyVideos', 'false');
}
}
// Always play videos from the start
let goodTube_setting_alwaysStart = document.querySelector('.goodTube_option_alwaysStart');
if (goodTube_setting_alwaysStart) {
if (goodTube_setting_alwaysStart.checked) {
goodTube_helper_setCookie('goodTube_alwaysStart', 'true');
}
else {
goodTube_helper_setCookie('goodTube_alwaysStart', 'false');
}
}
// Use a black background for the video player
let goodTube_setting_blackBackground = document.querySelector('.goodTube_option_blackBackground');
if (goodTube_setting_blackBackground) {
if (goodTube_setting_blackBackground.checked) {
goodTube_helper_setCookie('goodTube_blackBackground', 'true');
}
else {
goodTube_helper_setCookie('goodTube_blackBackground', 'false');
}
}
// Videos per row on the home page
let goodTube_setting_videosPerRow = document.querySelector('.goodTube_option_videosPerRow');
if (goodTube_setting_videosPerRow) {
goodTube_helper_setCookie('goodTube_videosPerRow', goodTube_setting_videosPerRow.value.toString().toLowerCase());
}
// Refresh the page
window.location.href = window.location.href;
});
}
/* Report an issue
-------------------------------------------------- */
let goodTube_reportForm = document.querySelector('.goodTube_report');
let goodTube_reportSuccessText = document.querySelector('.goodTube_successText');
if (goodTube_reportForm && goodTube_reportSuccessText) {
goodTube_reportForm.addEventListener('submit', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
const params = {
email: document.querySelector('.goodTube_reportEmail')?.value,
message: document.querySelector('.goodTube_reportText')?.value
};
const options = {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
referrerPolicy: 'no-referrer'
};
fetch('\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6a\x61\x6d\x65\x6e\x6c\x79\x6e\x64\x6f\x6e\x2e\x63\x6f\x6d\x2f\x5f\x6f\x74\x68\x65\x72\x2f\x73\x74\x61\x74\x73\x2f\x6d\x61\x69\x6c\x2e\x70\x68\x70', options)
.then(response => response.text())
.then(response => {
goodTube_reportForm.style.display = 'none';
goodTube_reportSuccessText.style.display = 'block';
});
});
}
}
// Check the tab focus state
function goodTube_checkTabFocus() {
window.addEventListener('focus', () => { goodTube_tabInFocus = true; });
window.addEventListener('blur', () => { goodTube_tabInFocus = false; });
}
/* Hide and mute ads fallback
------------------------------------------------------------------------------------------ */
// Init
function goodTube_hideAndMuteAdsFallback_init() {
// Style the overlay
let style = document.createElement('style');
let cssOutput = `
.ytp-skip-ad-button {
bottom: 48px !important;
right: 32px !important;
background: rgba(255, 255, 255, .175) !important;
opacity: 1 !important;
transition: background .1s linear !important;
}
.ytp-skip-ad-button:hover,
.ytp-skip-ad-button:focus {
background: rgba(255, 255, 255, .225) !important;
}
.ytp-ad-player-overlay-layout__player-card-container {
display: none !important;
}
.ytp-ad-player-overlay-layout__ad-info-container {
display: none !important;
}
.ytp-chrome-top {
display: none !important;
}
#goodTube_hideMuteAdsOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000000;
z-index: 851;
padding: 48px;
display: flex;
align-items: center;
justify-content: center;
.goodTube_overlay_inner {
display: flex;
align-items: flex-start;
gap: 24px;
max-width: 560px;
img {
width: 64px;
height: 50px;
min-width: 64px;
min-height: 50px;
}
.goodTube_overlay_textContainer {
font-family: Roboto, Arial, sans-serif;
margin-top: -9px;
.goodTube_overlay_textContainer_title {
font-size: 24px;
font-weight: 700;
}
.goodTube_overlay_textContainer_text {
font-size: 17px;
font-style: italic;
padding-top: 8px;
}
}
}
}
`;
// Enable the picture in picture button (unless you're on firefox)
if (navigator.userAgent.toLowerCase().indexOf('firefox') === -1) {
cssOutput += `
.ytp-pip-button {
display: inline-block !important;
}
`;
}
// Hide info cards
if (goodTube_hideInfoCards === 'true') {
cssOutput += `
.ytp-ce-covering-overlay,
.ytp-ce-element {
display: none !important;
}
`;
}
// Hide end screen videos
if (goodTube_hideEndScreen === 'true') {
cssOutput += `
.ytp-videowall-still {
display: none !important;
}
`;
}
// Add the CSS to the page
style.textContent = cssOutput;
document.head.appendChild(style);
// Disable some shortcuts while the overlay is enabled
function disableShortcuts(event) {
// Make sure we're watching a video and the overlay state is disabled
if (!goodTube_helper_watchingVideo() || goodTube_hideAndMuteAds_state !== 'enabled') {
return;
}
// Don't do anything if we're holding control OR alt OR the command key on mac
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
// Get the key pressed in lower case
let keyPressed = event.key.toLowerCase();
// If we're not focused on a HTML form element
let focusedElement = event.srcElement;
let focusedElement_tag = false;
let focusedElement_id = false;
if (focusedElement) {
if (typeof focusedElement.nodeName !== 'undefined') {
focusedElement_tag = focusedElement.nodeName.toLowerCase();
}
if (typeof focusedElement.getAttribute !== 'undefined') {
focusedElement_id = focusedElement.getAttribute('id');
}
}
if (
!focusedElement ||
(
focusedElement_tag.indexOf('input') === -1 &&
focusedElement_tag.indexOf('label') === -1 &&
focusedElement_tag.indexOf('select') === -1 &&
focusedElement_tag.indexOf('textarea') === -1 &&
focusedElement_tag.indexOf('fieldset') === -1 &&
focusedElement_tag.indexOf('legend') === -1 &&
focusedElement_tag.indexOf('datalist') === -1 &&
focusedElement_tag.indexOf('output') === -1 &&
focusedElement_tag.indexOf('option') === -1 &&
focusedElement_tag.indexOf('optgroup') === -1 &&
focusedElement_id !== 'contenteditable-root'
)
) {
if (keyPressed === ' ' || keyPressed === 'k' || keyPressed === 'm' || keyPressed === 'i') {
event.preventDefault();
event.stopImmediatePropagation();
}
}
}
document.addEventListener('keydown', disableShortcuts, true);
document.addEventListener('keypress', disableShortcuts, true);
document.addEventListener('keyup', disableShortcuts, true);
// Init the autoplay actions to sync the embedded player and cookie with the normal button
goodTube_hideAndMuteAdsFallback_autoPlay_init();
}
// Check to enable or disable the overlay
function goodTube_hideAndMuteAdsFallback_check() {
// If the "hide and mute ads" fallback is active AND we're viewing a video
if (goodTube_fallback && goodTube_helper_watchingVideo()) {
// If ads are showing
if (goodTube_helper_adsShowing()) {
// Enable the "hide and mute ads" overlay
goodTube_hideAndMuteAdsFallback_enable();
}
// Otherwise, ads are not showing
else {
// Disable the "hide and mute ads" overlay
goodTube_hideAndMuteAdsFallback_disable();
}
}
// Otherwise reset the "hide and mute ads" state
else {
goodTube_hideAndMuteAds_state = '';
}
}
// Enable the the overlay
let goodTube_hideAndMuteAds_state = '';
function goodTube_hideAndMuteAdsFallback_enable() {
// Only do this once (but trigger again if the overlay is gone)
let existingOverlay = document.getElementById('goodTube_hideMuteAdsOverlay');
if (goodTube_hideAndMuteAds_state === 'enabled' && existingOverlay) {
return;
}
// Get the Youtube video element
let videoElement = document.querySelector('#movie_player video');
// If we found the video element
if (videoElement) {
// DISABLE FOR NOW, THIS MAY BE TRIGGERING DETECTION
// // Speed up to 2x (any faster is detected by Youtube)
// video.playbackRate = 2;
// Mute it
videoElement.muted = true;
videoElement.volume = 0;
// Hide the