您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Watch age restricted videos on YouTube without login and without age verification :)
当前为
// ==UserScript== // @name Simple YouTube Age Restriction Bypass // @description Watch age restricted videos on YouTube without login and without age verification :) // @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen :) // @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge :) // @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età :) // @version 2.1.2 // @author Zerody (https://github.com/zerodytrash) // @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/ // @supportURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues // @license MIT // @match https://www.youtube.com/* // @match https://m.youtube.com/* // @grant none // @run-at document-start // @compatible chrome Chrome + Tampermonkey or Violentmonkey // @compatible firefox Firefox + Greasemonkey or Tampermonkey or Violentmonkey // @compatible opera Opera + Tampermonkey or Violentmonkey // @compatible edge Edge + Tampermonkey or Violentmonkey // @compatible safari Safari + Tampermonkey or Violentmonkey // ==/UserScript== const initUnlocker = function () { var nativeParse = window.JSON.parse; // Backup the original parse function var nativeDefineProperty = getNativeDefineProperty(); // Backup the original defineProperty function to intercept setter & getter on the ytInitialPlayerResponse var nativeXmlHttpOpen = XMLHttpRequest.prototype.open; var wrappedPlayerResponse = null; var wrappedNextResponse = null; var unlockablePlayerStates = ["AGE_VERIFICATION_REQUIRED", "AGE_CHECK_REQUIRED", "LOGIN_REQUIRED"]; var playerResponsePropertyAliases = ["ytInitialPlayerResponse", "playerResponse"]; var lastProxiedGoogleVideoUrlParams = null; var responseCache = {}; // YouTube API config (Innertube). // The actual values will be determined later from the global ytcfg variable => setInnertubeConfigFromYtcfg() var innertubeConfig = { INNERTUBE_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", INNERTUBE_CLIENT_NAME: "WEB", INNERTUBE_CLIENT_VERSION: "2.20210721.00.00", INNERTUBE_CONTEXT: {}, STS: 18834, // signatureTimestamp (relevant for the cipher functions) LOGGED_IN: false }; // The following proxies are currently used as fallback if the innertube age-gate bypass doesn't work... // You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy var accountProxyServerHost = "https://youtube-proxy.zerody.one"; var videoProxyServerHost = "https://phx.4everproxy.com"; // UI-related stuff (notifications, ...) var enableUnlockNotification = true; var playerCreationObserver = null; var notificationElement = null; var notificationTimeout = null; // Just for compatibility: Backup original getter/setter for 'ytInitialPlayerResponse', defined by other extensions like AdBlock var initialPlayerResponseDescriptor = window.Object.getOwnPropertyDescriptor(window, "ytInitialPlayerResponse"); var chainedPlayerSetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.set : null; var chainedPlayerGetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.get : null; // Just for compatibility: Intercept (re-)definitions on YouTube's initial player response property to chain setter/getter from other extensions by hijacking the Object.defineProperty function window.Object.defineProperty = function (obj, prop, descriptor) { if (obj === window && playerResponsePropertyAliases.includes(prop)) { console.info("Another extension tries to redefine '" + prop + "' (probably an AdBlock extension). Chain it..."); if (descriptor && descriptor.set) chainedPlayerSetter = descriptor.set; if (descriptor && descriptor.get) chainedPlayerGetter = descriptor.get; } else { nativeDefineProperty(obj, prop, descriptor); } } // Redefine 'ytInitialPlayerResponse' to inspect and modify the initial player response as soon as the variable is set on page load nativeDefineProperty(window, "ytInitialPlayerResponse", { set: function (playerResponse) { // prevent recursive setter calls by ignoring unchanged data (this fixes a problem caused by Brave browser shield) if (playerResponse === wrappedPlayerResponse) return; wrappedPlayerResponse = inspectJsonData(playerResponse); if (typeof chainedPlayerSetter === "function") chainedPlayerSetter(wrappedPlayerResponse); }, get: function () { if (typeof chainedPlayerGetter === "function") try { return chainedPlayerGetter() } catch (err) { }; return wrappedPlayerResponse || {}; }, configurable: true }); // Also redefine 'ytInitialData' for the initial next/sidebar response nativeDefineProperty(window, "ytInitialData", { set: function (nextResponse) { wrappedNextResponse = inspectJsonData(nextResponse); }, get: function () { return wrappedNextResponse; }, configurable: true }); // Intercept XMLHttpRequest.open to rewrite video URL's (sometimes required) XMLHttpRequest.prototype.open = function () { if (arguments.length > 1 && typeof arguments[1] === "string" && arguments[1].indexOf("https://") === 0) { var method = arguments[0]; var url = new URL(arguments[1]); var urlParams = new URLSearchParams(url.search); // If the account proxy was used to retieve the video info, the following applies: // some video files (mostly music videos) can only be accessed from IPs in the same country as the innertube api request (/youtubei/v1/player) was made. // to get around this, the googlevideo URL will be replaced with a web-proxy URL in the same country (US). // this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url... function isGoogleVideo() { return method === "GET" && url.host.indexOf(".googlevideo.com") > 0; } function hasGcrFlag() { return urlParams.get("gcr") !== null; } function isUnlockedByAccountProxy() { return urlParams.get("id") !== null && lastProxiedGoogleVideoUrlParams && urlParams.get("id") === lastProxiedGoogleVideoUrlParams.get("id"); } if (videoProxyServerHost && isGoogleVideo() && hasGcrFlag() && isUnlockedByAccountProxy()) { // rewrite request URL arguments[1] = videoProxyServerHost + "/direct/" + btoa(arguments[1]); // solve CORS errors by preventing YouTube from enabling the "withCredentials" option (not required for the proxy) nativeDefineProperty(this, "withCredentials", { set: function () { }, get: function () { return false; } }); } } return nativeXmlHttpOpen.apply(this, arguments); } // Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function window.JSON.parse = function (text, reviver) { return inspectJsonData(nativeParse(text, reviver)); } function inspectJsonData(parsedData) { // If YouTube does JSON.parse(null) or similar weird things if (typeof parsedData !== "object" || parsedData === null) return parsedData; try { // Unlock #1: Array based in "&pbj=1" AJAX response on any navigation (does not seem to be used anymore) if (Array.isArray(parsedData)) { var playerResponseArrayItem = parsedData.find(e => typeof e.playerResponse === "object"); var playerResponse = playerResponseArrayItem?.playerResponse; if (playerResponse && isAgeRestricted(playerResponse.playabilityStatus)) { playerResponseArrayItem.playerResponse = unlockPlayerResponse(playerResponse); var nextResponseArrayItem = parsedData.find(e => typeof e.response === "object"); var nextResponse = nextResponseArrayItem?.response; if (isWatchNextObject(nextResponse) && !isLoggedIn() && isWatchNextSidebarEmpty(nextResponse.contents)) { nextResponseArrayItem.response = unlockNextResponse(nextResponse); } } } // Hide unlock notification on navigation (if still visible from the last unlock) if (parsedData.playerResponse || parsedData.playabilityStatus) hidePlayerNotification(); // Unlock #2: Another JSON-Object containing the 'playerResponse' (seems to be used by m.youtube.com with &pbj=1) if (parsedData.playerResponse?.playabilityStatus && parsedData.playerResponse?.videoDetails && isAgeRestricted(parsedData.playerResponse.playabilityStatus)) { parsedData.playerResponse = unlockPlayerResponse(parsedData.playerResponse); } // Unlock #3: Initial page data structure and response from the '/youtubei/v1/player' endpoint if (parsedData.playabilityStatus && parsedData.videoDetails && isAgeRestricted(parsedData.playabilityStatus)) { parsedData = unlockPlayerResponse(parsedData); } // Equivelant of unlock #2 for sidebar/next response if (isWatchNextObject(parsedData.response) && !isLoggedIn() && isWatchNextSidebarEmpty(parsedData.response.contents)) { parsedData.response = unlockNextResponse(parsedData.response); } // Equivelant of unlock #3 for sidebar/next response if (isWatchNextObject(parsedData) && !isLoggedIn() && isWatchNextSidebarEmpty(parsedData.contents)) { parsedData = unlockNextResponse(parsedData) } } catch (err) { console.error("Simple-YouTube-Age-Restriction-Bypass-Error:", err, "You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues"); } return parsedData; } function isAgeRestricted(playabilityStatus) { if (!playabilityStatus || !playabilityStatus.status) return false; return typeof playabilityStatus.desktopLegacyAgeGateReason !== "undefined" || unlockablePlayerStates.includes(playabilityStatus.status); } function isWatchNextObject(parsedData) { if (!parsedData?.contents) return false; if (!parsedData?.currentVideoEndpoint?.watchEndpoint?.videoId) return false; return parsedData.contents.twoColumnWatchNextResults || parsedData.contents.singleColumnWatchNextResults; } function isWatchNextSidebarEmpty(contents) { var secondaryResults = contents.twoColumnWatchNextResults?.secondaryResults?.secondaryResults; if (secondaryResults && secondaryResults.results) return false; // MWEB response layout var singleColumnWatchNextContents = contents.singleColumnWatchNextResults?.results?.results?.contents; if (!singleColumnWatchNextContents) return true; var itemSectionRendererArrayItem = singleColumnWatchNextContents.find(e => e.itemSectionRenderer?.targetId === "watch-next-feed"); var itemSectionRenderer = itemSectionRendererArrayItem?.itemSectionRenderer; return typeof itemSectionRenderer === "undefined"; } function isLoggedIn() { setInnertubeConfigFromYtcfg(); return innertubeConfig.LOGGED_IN; } function unlockPlayerResponse(playerResponse) { var videoId = playerResponse.videoDetails.videoId; var reason = playerResponse.playabilityStatus?.status; var unlockedPayerResponse = getUnlockedPlayerResponse(videoId, reason); // account proxy error? if (unlockedPayerResponse.errorMessage) { showPlayerNotification("#7b1e1e", "Unable to unlock this video :( Please look into the developer console for more details. (ProxyError)", 10); throw new Error(`Unlock Failed, errorMessage:${unlockedPayerResponse.errorMessage}; innertubeApiKey:${innertubeConfig.INNERTUBE_API_KEY}; innertubeClientName:${innertubeConfig.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${innertubeConfig.INNERTUBE_CLIENT_VERSION}`); } // check if the unlocked response isn't playable if (unlockedPayerResponse.playabilityStatus?.status !== "OK") { showPlayerNotification("#7b1e1e", `Unable to unlock this video :( Please look into the developer console for more details. (playabilityStatus: ${unlockedPayerResponse.playabilityStatus?.status})`, 10); throw new Error(`Unlock Failed, playabilityStatus:${unlockedPayerResponse.playabilityStatus?.status}; innertubeApiKey:${innertubeConfig.INNERTUBE_API_KEY}; innertubeClientName:${innertubeConfig.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${innertubeConfig.INNERTUBE_CLIENT_VERSION}`); } // if the video info was retrieved via proxy, store the URL params from the url- or signatureCipher-attribute to detect later if the requested video files are from this unlock. // => see isUnlockedByAccountProxy() if (unlockedPayerResponse.proxied && unlockedPayerResponse.streamingData?.adaptiveFormats) { var videoUrl = unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.url)?.url; var cipherText = unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.signatureCipher)?.signatureCipher; if (cipherText) videoUrl = new URLSearchParams(cipherText).get("url"); lastProxiedGoogleVideoUrlParams = videoUrl ? new URLSearchParams(new URL(videoUrl).search) : null; } showPlayerNotification("#005c04", "Age-restricted video successfully unlocked!", 4); return unlockedPayerResponse; } function unlockNextResponse(nextResponse) { var watchEndpoint = nextResponse.currentVideoEndpoint.watchEndpoint; var videoId = watchEndpoint.videoId; var playlistId = watchEndpoint.playlistId; var playlistIndex = watchEndpoint.index; var unlockedNextResponse = getUnlockedNextResponse(videoId, playlistId, playlistIndex); // check if the unlocked response's sidebar is still empty if (isWatchNextSidebarEmpty(unlockedNextResponse.contents)) { throw new Error(`Sidebar Unlock Failed, innertubeApiKey:${innertubeConfig.INNERTUBE_API_KEY}; innertubeClientName:${innertubeConfig.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${innertubeConfig.INNERTUBE_CLIENT_VERSION}`); } // Transfer WatchNextResults to original response if (nextResponse.contents?.twoColumnWatchNextResults?.secondaryResults) { nextResponse.contents.twoColumnWatchNextResults.secondaryResults = unlockedNextResponse?.contents?.twoColumnWatchNextResults?.secondaryResults; } // Transfer mobile (MWEB) WatchNextResults to original response if (nextResponse.contents?.singleColumnWatchNextResults?.results?.results?.contents) { var unlockedWatchNextFeed = unlockedNextResponse?.contents?.singleColumnWatchNextResults?.results?.results?.contents?.find(x => x.itemSectionRenderer?.targetId === "watch-next-feed"); if (unlockedWatchNextFeed) nextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(unlockedWatchNextFeed); } return nextResponse; } function getUnlockedPlayerResponse(videoId, reason) { // Check if response is cached if (responseCache.videoId === videoId) return responseCache.content; setInnertubeConfigFromYtcfg(); var playerResponse = null; // Strategy 1: Retrieve the video info by using a age-gate bypass for the innertube API // Source: https://github.com/yt-dlp/yt-dlp/issues/574#issuecomment-887171136 function useInnertubeEmbed() { console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Unlock Method #1 (Innertube Embed)"); var payload = getInnertubeEmbedPayload(videoId); var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", `/youtubei/v1/player?key=${innertubeConfig.INNERTUBE_API_KEY}`, false); // Synchronous!!! xmlhttp.send(JSON.stringify(payload)); playerResponse = nativeParse(xmlhttp.responseText); } // Strategy 2: Retrieve the video info from an account proxy server. // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy function useProxy() { console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Unlock Method #2 (Account Proxy)"); var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", accountProxyServerHost + `/getPlayer?videoId=${encodeURIComponent(videoId)}&reason=${encodeURIComponent(reason)}&clientVersion=${innertubeConfig.INNERTUBE_CLIENT_VERSION}&signatureTimestamp=${innertubeConfig.STS}`, false); // Synchronous!!! xmlhttp.send(null); playerResponse = nativeParse(xmlhttp.responseText); playerResponse.proxied = true; } if (playerResponse?.playabilityStatus?.status !== "OK") useInnertubeEmbed(); if (playerResponse?.playabilityStatus?.status !== "OK") useProxy(); // Cache response for 10 seconds responseCache = { videoId: videoId, content: playerResponse }; setTimeout(function () { responseCache = {} }, 10000); return playerResponse; } function getUnlockedNextResponse(videoId, playlistId, playlistIndex) { setInnertubeConfigFromYtcfg(); // Retrieve the video info by using a age-gate bypass for the innertube API // Source: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/16#issuecomment-889232425 console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Sidebar Unlock Method (Innertube Embed)"); var payload = getInnertubeEmbedPayload(videoId, playlistId, playlistIndex); var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", `/youtubei/v1/next?key=${innertubeConfig.INNERTUBE_API_KEY}`, false); // Synchronous!!! xmlhttp.send(JSON.stringify(payload)); return nativeParse(xmlhttp.responseText); } function getInnertubeEmbedPayload(videoId, playlistId, playlistIndex) { var data = { context: { client: { clientName: innertubeConfig.INNERTUBE_CLIENT_NAME.replace('_EMBEDDED_PLAYER', ''), clientVersion: innertubeConfig.INNERTUBE_CLIENT_VERSION, clientScreen: "EMBED" }, thirdParty: { embedUrl: "https://www.youtube.com/" } }, playbackContext: { contentPlaybackContext: { signatureTimestamp: innertubeConfig.STS } }, videoId: videoId, playlistId: playlistId, playlistIndex: playlistIndex } // Append client info from INNERTUBE_CONTEXT if (typeof innertubeConfig.INNERTUBE_CONTEXT?.client === "object") { data.context.client = Object.assign(innertubeConfig.INNERTUBE_CONTEXT.client, data.context.client); } return data; } // to avoid version conflicts between client and server response, the current YouTube version config will be determined function setInnertubeConfigFromYtcfg() { if (!window.ytcfg) { console.warn("Simple-YouTube-Age-Restriction-Bypass: Unable to retrieve global YouTube configuration (window.ytcfg). Using old values..."); return; } for (const key in innertubeConfig) { var value = window.ytcfg.data_?.[key] ?? window.ytcfg.get?.(key); if (typeof value !== "undefined" && value !== null) { innertubeConfig[key] = value; } else { console.warn(`Simple-YouTube-Age-Restriction-Bypass: Unable to retrieve global YouTube configuration variable '${key}'. Using old value...`); } } } function showPlayerNotification(color, message, displayDuration) { if (!enableUnlockNotification) return; if (typeof MutationObserver !== "function") return; try { // clear existing notifications disconnectPlayerCreationObserver(); hidePlayerNotification(); function getPlayerElement() { return document.querySelector("#primary > #primary-inner > #player") || document.querySelector("#player-container-id > #player"); } function createNotification() { var playerElement = getPlayerElement(); if (!playerElement) return; // first, remove existing notification hidePlayerNotification(); // create new notification notificationElement = document.createElement("div"); notificationElement.innerHTML = message; notificationElement.style = `width: 100%; text-align: center; background-color: ${color}; color: #ffffff; padding: 2px 0px 2px; font-size: 1.1em;`; notificationElement.id = "bypass-notification"; // append below the player playerElement.parentNode.insertBefore(notificationElement, playerElement.nextSibling); if (notificationTimeout) { clearTimeout(notificationTimeout); notificationTimeout = null; } notificationTimeout = setTimeout(hidePlayerNotification, displayDuration * 1000); } function disconnectPlayerCreationObserver() { if (playerCreationObserver) { playerCreationObserver.disconnect(); playerCreationObserver = null; } } // Does the player already exist in the DOM? if (getPlayerElement() !== null) { createNotification(); return; } // waiting for creation of the player element... playerCreationObserver = new MutationObserver(function (mutations) { if (getPlayerElement() !== null) { disconnectPlayerCreationObserver(); createNotification(); } }); playerCreationObserver.observe(document.body, { childList: true }); } catch (err) { } } function hidePlayerNotification() { if (playerCreationObserver) { playerCreationObserver.disconnect(); playerCreationObserver = null; } if (notificationElement) { notificationElement.remove(); notificationElement = null; } } // Some extensions like AdBlock override the Object.defineProperty function to prevent a redefinition of the 'ytInitialPlayerResponse' variable by YouTube. // But we need to define a custom descriptor to that variable to intercept his value. This behavior causes a race condition depending on the execution order with this script :( // This function tries to restore the native Object.defineProperty function... function getNativeDefineProperty() { // Check if the Object.defineProperty function is native (original) if (window.Object.defineProperty && window.Object.defineProperty.toString().indexOf("[native code]") > -1) { return window.Object.defineProperty; } // if the Object.defineProperty function is already overidden, try to restore the native function from another window... try { if (!document.body) document.body = document.createElement("body"); var tempFrame = document.createElement("iframe"); tempFrame.style.display = "none"; document.body.insertAdjacentElement("beforeend", tempFrame); var nativeDefineProperty = tempFrame.contentWindow.Object.defineProperty; tempFrame.remove(); console.info("Simple-YouTube-Age-Restriction-Bypass: Overidden Object.defineProperty function successfully restored!"); return nativeDefineProperty; } catch (err) { console.warn("Simple-YouTube-Age-Restriction-Bypass: Unable to restore the original Object.defineProperty function", err); return window.Object.defineProperty; } } }; // Just a trick to get around the sandbox restrictions in Firefox / Greasemonkey // Greasemonkey => Inject code into the main window // Tampermonkey & Violentmonkey => Execute code directly if(GM_info?.scriptHandler === "Greasemonkey") { window.eval("("+ initUnlocker.toString() +")();"); } else { initUnlocker(); }