From 72ec855fe64d663a7971377021df5f723fee523b Mon Sep 17 00:00:00 2001
From: drunkwinter <38593134+drunkwinter@users.noreply.github.com>
Date: Sat, 16 Nov 2024 11:05:42 +0100
Subject: [PATCH] [refactor] cleanup
Probably one of my last commits
---
.github/scripts/publish_to_greasyfork/main.ts | 15 +-
account-proxy/.gitignore | 1 -
account-proxy/README.md | 49 ----
account-proxy/controllers/proxyController.js | 94 --------
account-proxy/controllers/statsController.js | 13 -
account-proxy/lib/innertubeApi.js | 63 -----
account-proxy/lib/stats.js | 140 -----------
account-proxy/lib/types.js | 46 ----
account-proxy/lib/utils.js | 66 -----
account-proxy/package.json | 13 -
account-proxy/server.js | 49 ----
package.json | 6 +-
scripts/build.js | 55 +++--
src/components/confirmation.js | 24 --
src/components/endpoints/index.js | 2 -
src/components/endpoints/innertube.js | 32 ---
src/components/endpoints/proxy.js | 61 -----
src/components/errorScreen/index.js | 32 ---
.../errorScreen/templates/button.html | 3 -
src/components/innertube.js | 27 +++
src/components/inspectors/googlevideo.js | 11 -
src/components/inspectors/index.js | 4 -
src/components/inspectors/next.js | 19 --
src/components/inspectors/player.js | 27 ---
src/components/inspectors/search.js | 7 -
src/components/interceptors/generic.js | 14 --
src/components/interceptors/index.js | 5 -
src/components/interceptors/initialData.js | 61 -----
src/components/interceptors/json.js | 10 -
src/components/interceptors/natives.js | 2 -
src/components/interceptors/request.js | 32 ---
src/components/interceptors/xhrOpen.js | 27 ---
src/components/requestPreprocessor.js | 107 ---------
src/components/storage.js | 13 -
src/components/strategies/index.js | 2 -
src/components/strategies/next.js | 54 -----
src/components/strategies/player.js | 121 ----------
src/components/thumbnailFix.js | 44 +++-
src/components/toast/templates/desktop.html | 1 -
src/components/toast/templates/mobile.html | 5 -
src/components/ui/errorScreen.js | 48 ++++
.../{toast/index.js => ui/toast.js} | 17 +-
src/components/unlocker/index.js | 4 -
src/components/unlocker/next.js | 98 ++++++--
src/components/unlocker/player.js | 209 ++++++++++++----
src/config.js | 42 +---
src/{utils => }/logger.js | 18 +-
src/main.js | 227 +++++++++++++++---
src/utils.js | 94 ++++++++
src/utils/index.js | 156 ------------
userscript.config.js | 30 ---
51 files changed, 707 insertions(+), 1593 deletions(-)
delete mode 100644 account-proxy/.gitignore
delete mode 100644 account-proxy/README.md
delete mode 100644 account-proxy/controllers/proxyController.js
delete mode 100644 account-proxy/controllers/statsController.js
delete mode 100644 account-proxy/lib/innertubeApi.js
delete mode 100644 account-proxy/lib/stats.js
delete mode 100644 account-proxy/lib/types.js
delete mode 100644 account-proxy/lib/utils.js
delete mode 100644 account-proxy/package.json
delete mode 100644 account-proxy/server.js
delete mode 100644 src/components/confirmation.js
delete mode 100644 src/components/endpoints/index.js
delete mode 100644 src/components/endpoints/innertube.js
delete mode 100644 src/components/endpoints/proxy.js
delete mode 100644 src/components/errorScreen/index.js
delete mode 100644 src/components/errorScreen/templates/button.html
create mode 100644 src/components/innertube.js
delete mode 100644 src/components/inspectors/googlevideo.js
delete mode 100644 src/components/inspectors/index.js
delete mode 100644 src/components/inspectors/next.js
delete mode 100644 src/components/inspectors/player.js
delete mode 100644 src/components/inspectors/search.js
delete mode 100644 src/components/interceptors/generic.js
delete mode 100644 src/components/interceptors/index.js
delete mode 100644 src/components/interceptors/initialData.js
delete mode 100644 src/components/interceptors/json.js
delete mode 100644 src/components/interceptors/natives.js
delete mode 100644 src/components/interceptors/request.js
delete mode 100644 src/components/interceptors/xhrOpen.js
delete mode 100644 src/components/requestPreprocessor.js
delete mode 100644 src/components/storage.js
delete mode 100644 src/components/strategies/index.js
delete mode 100644 src/components/strategies/next.js
delete mode 100644 src/components/strategies/player.js
delete mode 100644 src/components/toast/templates/desktop.html
delete mode 100644 src/components/toast/templates/mobile.html
create mode 100644 src/components/ui/errorScreen.js
rename src/components/{toast/index.js => ui/toast.js} (76%)
delete mode 100644 src/components/unlocker/index.js
rename src/{utils => }/logger.js (60%)
create mode 100644 src/utils.js
delete mode 100644 src/utils/index.js
delete mode 100644 userscript.config.js
diff --git a/.github/scripts/publish_to_greasyfork/main.ts b/.github/scripts/publish_to_greasyfork/main.ts
index 423ce69..82bacf3 100644
--- a/.github/scripts/publish_to_greasyfork/main.ts
+++ b/.github/scripts/publish_to_greasyfork/main.ts
@@ -18,9 +18,12 @@ async function main() {
const greasyfork_script_type = (() => {
switch (process.env.GREASYFORK_SCRIPT_TYPE) {
- case 'public': return '1';
- case 'unlisted': return '2';
- case 'library': return '3';
+ case 'public':
+ return '1';
+ case 'unlisted':
+ return '2';
+ case 'library':
+ return '3';
}
})();
if (!greasyfork_script_type) {
@@ -56,7 +59,7 @@ async function publish_to_greasyfork(user_email: string, user_pass: string, scri
const BASE_URL = 'https://greasyfork.org';
// "/en/search" appears to be the lightest page
- const LIGHTEST_PAGE_URL= `${BASE_URL}/en/search`;
+ const LIGHTEST_PAGE_URL = `${BASE_URL}/en/search`;
// Get initial page to retrieve the initial tokens
const initial_response = await fetch(LIGHTEST_PAGE_URL);
@@ -70,7 +73,7 @@ async function publish_to_greasyfork(user_email: string, user_pass: string, scri
throw new Error('Could not retrieve initial authenticity token');
}
- const login_request_url= `${BASE_URL}/en/users/sign_in`;
+ const login_request_url = `${BASE_URL}/en/users/sign_in`;
const login_request_options: RequestInit = {
method: 'POST',
@@ -130,7 +133,7 @@ async function publish_to_greasyfork(user_email: string, user_pass: string, scri
update_body.set('script[adult_content_self_report]', '0');
update_body.set('commit', 'Post new version');
- const upload_request_url= `${BASE_URL}/en/scripts/${script_id}/versions`;
+ const upload_request_url = `${BASE_URL}/en/scripts/${script_id}/versions`;
const upload_request_options: RequestInit = {
method: 'POST',
diff --git a/account-proxy/.gitignore b/account-proxy/.gitignore
deleted file mode 100644
index 497d582..0000000
--- a/account-proxy/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-stats.json
diff --git a/account-proxy/README.md b/account-proxy/README.md
deleted file mode 100644
index 6d3ea95..0000000
--- a/account-proxy/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-## YouTube Account Proxy
-
-Note: This is not a part of the browser script!
-
If you only want to watch age-restricted videos, follow these instructions.
-
-This is the account proxy server to access age-restricted videos via an age-verified or US-located YouTube account. You can run your own account proxy server instance and include the cookies of a YouTube account in the configuration. This allows you to share the account with others to access age-restricted videos.
-
-### Installation
-1. Install required dependencies with `npm install`.
-2. Create the `.env` configuration (see below) and paste the required cookies from your YouTube account.
-> The YouTube account must be verified in order to unlock some age-restricted videos.
-3. Start the server with `npm run proxy:start` or `npm start` in `/account-proxy`.
-
-> If your account is not age-verified and you don't have an IP from the USA, you might need to use an http(s) proxy located in that region.
-
-``.env`` configuration:
-
-````ini
-PORT=8089
-
-# Trust X-Forwarded-* headers? set this to 1 if you use a reverse proxy like Nginx with a corresponding configuration.
-ENABLE_TRUST_PROXY=0
-
-# Define if you want to collect anonymous usage statistics to monitor the server load
-# The statistics can be retrieved via the `/getStats` endpoint in JSON format
-ENABLE_STATS=1
-
-# Cookie values (required)
-SID=
-HSID=
-SSID=
-APISID=
-SAPISID=
-PSIDTS=
-
-# Proxy (https)
-# PROXY=
-````
-
-### Logging
-
-If you run your own proxy instance then you should respect the privacy of the users and reduce all log activities to a minimum.
-
-Concretely this means:
-- Do not log IP Addresses
-- Do not log Video-IDs
-
-The account proxy itself does not log any user-related data by default. However, it could be that your reverse proxy is logging the requests (if you use one).
-For Nginx you can disable the log functionality as follows: https://thisinterestsme.com/disable-nginx-access-log/
diff --git a/account-proxy/controllers/proxyController.js b/account-proxy/controllers/proxyController.js
deleted file mode 100644
index 58e4759..0000000
--- a/account-proxy/controllers/proxyController.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import dotenv from 'dotenv';
-import { ProxyAgent } from 'undici';
-
-import { extractAttributes, getYoutubeResponseStatus, checkForGcrFlag } from '../lib/utils.js';
-import { YouTubeCredentials, YouTubeClientParams } from '../lib/types.js';
-import innertubeApi from '../lib/innertubeApi.js';
-import stats from '../lib/stats.js';
-
-dotenv.config();
-
-const credentials = new YouTubeCredentials();
-const proxy = process.env.PROXY;
-const proxyAgent = proxy ? new ProxyAgent(proxy) : undefined;
-
-function getPlayer(req, res) {
- handleProxyRequest(req, res, 'player');
-}
-
-function getNext(req, res) {
- handleProxyRequest(req, res, 'next');
-}
-
-async function handleProxyRequest(req, res, endpoint) {
- const tsStart = new Date().getTime();
-
- try {
- const clientParams = new YouTubeClientParams();
- clientParams.fromRequest(req);
- clientParams.validate();
-
- stats.countRequest(clientParams.reason, clientParams.clientName, req.headers['cf-ipcountry'], req.headers['origin']);
-
- // Hotfix for embed player
- if (clientParams.clientName === 'WEB_EMBEDDED_PLAYER') {
- clientParams.clientName = 'WEB';
- clientParams.clientVersion = '2.20220228.01.00';
- }
-
- const pRequests = [innertubeApi.sendApiRequest(endpoint, clientParams, credentials, proxyAgent)];
-
- // Include /next (sidebar) if flag set
- if (clientParams.includeNext) {
- pRequests.push(innertubeApi.sendApiRequest('next', clientParams, credentials, proxyAgent));
- }
-
- const [playerResponse, nextResponse] = await Promise.all(pRequests);
-
- if (!playerResponse || typeof playerResponse !== 'object') {
- throw new Error(`Invalid YouTube response received for endpoint /player`);
- }
-
- if (clientParams.includeNext && (!nextResponse || typeof nextResponse !== 'object')) {
- throw new Error(`Invalid YouTube response received for endpoint /next`);
- }
-
- const youtubeStatus = getYoutubeResponseStatus(playerResponse);
- const youtubeGcrFlagSet = checkForGcrFlag(playerResponse);
-
- stats.countResponse('player', youtubeStatus, youtubeGcrFlagSet);
-
- const responseData = extractAttributes(playerResponse, ['playabilityStatus', 'videoDetails', 'streamingData']);
-
- responseData.proxy = { clientParams, youtubeGcrFlagSet, youtubeStatus };
-
- /**
- * Workaround: when we provide `adaptiveFormats` the client cannot playback the video.
- *
- * It seems the URLs we get here or the one the client constructs from these URLs are tied to the requesting account.
- * The low quality `formats` URLs seem fine.
- */
- delete responseData.streamingData.adaptiveFormats;
-
- if (nextResponse) {
- stats.countResponse('next', getYoutubeResponseStatus(nextResponse), null);
- responseData.nextResponse = extractAttributes(nextResponse, ['contents', 'engagementPanels']);
- }
-
- res.code(200).send(responseData);
- } catch (err) {
- if (err instanceof Error) {
- console.error(endpoint, err.message);
- res.code(500).send({ errorMessage: err.message });
- stats.countResponse(endpoint, 'EXCEPTION');
- stats.countException(endpoint, err.message);
- }
- }
-
- stats.countLatency(new Date().getTime() - tsStart);
-}
-
-export default {
- getPlayer,
- getNext
-}
diff --git a/account-proxy/controllers/statsController.js b/account-proxy/controllers/statsController.js
deleted file mode 100644
index 681e816..0000000
--- a/account-proxy/controllers/statsController.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import stats from '../lib/stats.js';
-
-function getStats(req, res) {
- res.header('Cache-Control', 'no-store');
-
- return req.query.historical
- ? stats.getHistoricalStats()
- : stats.getTodayStats();
-}
-
-export default {
- getStats
-}
diff --git a/account-proxy/lib/innertubeApi.js b/account-proxy/lib/innertubeApi.js
deleted file mode 100644
index 7c12784..0000000
--- a/account-proxy/lib/innertubeApi.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import crypto from 'node:crypto';
-
-import { fetch } from 'undici';
-
-const generateSidBasedAuth = function (sapisid) {
- const timestamp = Math.floor(new Date().getTime() / 1000);
- const hashInput = timestamp + " " + sapisid + " " + "https://www.youtube.com";
- const hashDigest = crypto.createHash("sha1").update(hashInput).digest("hex");
- return `SAPISIDHASH ${timestamp}_${hashDigest}`;
-}
-
-const generateApiRequestData = function (clientParams) {
- return {
- "videoId": clientParams.videoId,
- "context": {
- "client": {
- "hl": clientParams.hl,
- "gl": "US",
- "clientName": clientParams.clientName,
- "clientVersion": clientParams.clientVersion,
- "userInterfaceTheme": clientParams.userInterfaceTheme,
- },
- },
- "playbackContext": {
- "contentPlaybackContext": {
- "signatureTimestamp": clientParams.signatureTimestamp,
- }
- },
- "racyCheckOk": true,
- "contentCheckOk": true,
- "startTimeSecs": clientParams.startTimeSecs,
- }
-}
-
-const generateApiRequestHeaders = function (credentials) {
- return {
- "cookie": `SID=${credentials.SID}; HSID=${credentials.HSID}; SSID=${credentials.SSID}; APISID=${credentials.APISID}; SAPISID=${credentials.SAPISID}; __Secure-1PSIDTS=${credentials.PSIDTS};`,
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
- "content-type": "application/json",
- "authorization": generateSidBasedAuth(credentials.SAPISID),
- "origin": "https://www.youtube.com",
- }
-}
-
-const sendApiRequest = async function (endpoint, clientParams, credentials, proxyAgent) {
- const url = `https://www.youtube.com/youtubei/v1/${endpoint}?prettyPrint=false`;
- const headers = generateApiRequestHeaders(credentials);
- const data = generateApiRequestData(clientParams);
-
- const res = await fetch(url, {
- method: 'POST',
- headers,
- body: JSON.stringify(data),
- dispatcher: proxyAgent,
- signal: AbortSignal.timeout(5000),
- });
-
- return res.json();
-}
-
-export default {
- sendApiRequest
-}
diff --git a/account-proxy/lib/stats.js b/account-proxy/lib/stats.js
deleted file mode 100644
index f31c845..0000000
--- a/account-proxy/lib/stats.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import fs from 'node:fs';
-
-const statsFileName = 'stats.json';
-
-let initialized = false;
-
-let stats = {};
-
-let requestsLastMinute = 0;
-let requestsThisMinute = 0;
-
-let latencyMs = 0;
-let latencyMsSumUp = 0;
-let latencyRequestCount = 0;
-
-let playabilityHistory = [];
-
-function init() {
- // Load stats from file
- try {
- stats = JSON.parse(fs.readFileSync(statsFileName));
- } catch (err) { }
-
- // Save stats to file
- setInterval(() => {
- try {
- fs.writeFile(statsFileName, JSON.stringify(stats), () => { });
- } catch (err) { }
- }, 10 * 60 * 1000)
-
-
- setInterval(() => {
- // Reset request count every minute
- requestsLastMinute = requestsThisMinute;
- requestsThisMinute = 0;
-
- // Calc latency every minute
- latencyMs = latencyRequestCount > 0 ? (latencyMsSumUp / latencyRequestCount) : 0;
- latencyMsSumUp = 0;
- latencyRequestCount = 0;
- }, 60 * 1000)
-
- initialized = true;
-}
-
-function getTodayKey() {
- return (new Date()).toISOString().split('T')[0];
-}
-
-function countDayMetric(metric, key) {
- const day = getTodayKey();
-
- if (!stats.days) stats.days = {};
- if (!stats.days[day]) stats.days[day] = {};
- if (!stats.days[day][metric]) stats.days[day][metric] = {};
- if (!stats.days[day][metric][key]) stats.days[day][metric][key] = 0;
-
- stats.days[day][metric][key] += 1;
-}
-
-
-function countRequest(reason, clientName, country, origin) {
- if (!initialized) return;
-
- requestsThisMinute += 1;
-
- if (typeof reason === 'string' && reason !== 'UNKNOWN') {
- countDayMetric('reasons', reason.toUpperCase());
- }
-
- if (typeof clientName == 'string') {
- countDayMetric('clients', clientName.toUpperCase());
- }
-
- if (typeof country === 'string') {
- countDayMetric('countries', country.toUpperCase());
- }
-
- if (typeof origin === 'string') {
- countDayMetric('origins', origin);
- }
-}
-
-function countResponse(endpoint, status, containsGcrFlag) {
- if (!initialized) return;
-
- const key = `${endpoint.toUpperCase()}:${status}`;
- countDayMetric('responseResults', key);
-
- if (typeof containsGcrFlag === 'boolean') {
- countDayMetric('responseContainsGcrFlag', containsGcrFlag ? "YES" : "NO");
- }
-
- if (endpoint === 'player') {
- playabilityHistory.push(status);
- playabilityHistory = playabilityHistory.slice(-50);
- }
-}
-
-function countException(endpoint, message) {
- if (!initialized) return;
-
- const key = `${endpoint.toUpperCase()}:${message}`;
- countDayMetric('exceptions', key);
-}
-
-function countLatency(milliSecs) {
- if (!initialized) return;
-
- latencyMsSumUp += milliSecs;
- latencyRequestCount += 1;
-}
-
-function getRequestsPerMinute() {
- return requestsLastMinute;
-}
-
-function getHistoricalStats() {
- return stats;
-}
-
-function getTodayStats() {
- return {
- latencyMs: Math.round(latencyMs),
- requestsPerMinute: getRequestsPerMinute(),
- ...(stats.days?.[getTodayKey()] || {}),
- playabilityHistory
- };
-}
-
-export default {
- init,
- countRequest,
- countResponse,
- countException,
- countLatency,
- getRequestsPerMinute,
- getHistoricalStats,
- getTodayStats
-}
diff --git a/account-proxy/lib/types.js b/account-proxy/lib/types.js
deleted file mode 100644
index 0370ece..0000000
--- a/account-proxy/lib/types.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import dotenv from 'dotenv';
-
-import { fillObjectFromRequest, validateObjectAttributes } from './utils.js';
-
-dotenv.config();
-
-class YouTubeCredentials {
- constructor() {
- this.SID = process.env.SID;
- this.HSID = process.env.HSID;
- this.SSID = process.env.SSID;
- this.APISID = process.env.APISID;
- this.SAPISID = process.env.SAPISID;
- this.PSIDTS = process.env.PSIDTS;
-
- validateObjectAttributes(this);
- }
-}
-
-class YouTubeClientParams {
- constructor() {
- // Default values
- this.videoId = null;
- this.reason = 'UNKNOWN';
- this.clientName = 'WEB';
- this.clientVersion = '2.20210721.00.00';
- this.signatureTimestamp = 18834;
- this.hl = 'en';
- this.startTimeSecs = 0;
- this.userInterfaceTheme = 'USER_INTERFACE_THEME_DARK';
- this.includeNext = false;
- }
-
- fromRequest(request) {
- fillObjectFromRequest(this, request);
- }
-
- validate() {
- validateObjectAttributes(this);
- }
-}
-
-export {
- YouTubeCredentials,
- YouTubeClientParams
-}
diff --git a/account-proxy/lib/utils.js b/account-proxy/lib/utils.js
deleted file mode 100644
index 9a6b753..0000000
--- a/account-proxy/lib/utils.js
+++ /dev/null
@@ -1,66 +0,0 @@
-function fillObjectFromRequest(obj, request) {
- for (const propName in obj) {
- if (!(propName in request.query) || request.query[propName] === 'undefined') {
- continue;
- }
-
- switch(typeof obj[propName]) {
- case 'number':
- obj[propName] = parseInt(request.query[propName]);
- break;
- case 'boolean':
- obj[propName] = request.query[propName] === '1' || request.query[propName] === 'true';
- break;
- default:
- obj[propName] = request.query[propName];
- }
- }
-}
-
-function validateObjectAttributes(obj) {
- for (const propName in obj) {
- if (obj[propName] === null || obj[propName] === '') {
- throw new Error(`Missing value for ${propName}`);
- }
-
- if (typeof obj[propName] === 'number' && isNaN(obj[propName])) {
- throw new Error(`Invalid number value for ${propName}`);
- }
- }
-}
-
-function extractAttributes(obj, attributesArray) {
- let newObj = {};
-
- for (const i in attributesArray) {
- let attr = attributesArray[i];
- if (obj[attr]) {
- newObj[attr] = obj[attr];
- }
- }
-
- return newObj;
-}
-
-function getYoutubeResponseStatus(youtubeResponse) {
- return youtubeResponse.playabilityStatus?.status
- ?? (youtubeResponse.status ? `HTTP${youtubeResponse.status}` : 'unknown');
-}
-
-function checkForGcrFlag(youtubeData) {
- if (typeof youtubeData.streamingData !== 'object') {
- return;
- }
-
- let streamingDataJson = JSON.stringify(youtubeData.streamingData);
-
- return streamingDataJson.includes('gcr=') || streamingDataJson.includes('gcr%3D');
-}
-
-export {
- fillObjectFromRequest,
- validateObjectAttributes,
- extractAttributes,
- getYoutubeResponseStatus,
- checkForGcrFlag
-}
diff --git a/account-proxy/package.json b/account-proxy/package.json
deleted file mode 100644
index b8b747f..0000000
--- a/account-proxy/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "type": "module",
- "scripts": {
- "start": "node server.js"
- },
- "dependencies": {
- "@fastify/cors": "^9.0.1",
- "@fastify/rate-limit": "^9.1.0",
- "dotenv": "^16.4.5",
- "fastify": "^4.28.1",
- "undici": "^6.19.5"
- }
-}
diff --git a/account-proxy/server.js b/account-proxy/server.js
deleted file mode 100644
index 16ae781..0000000
--- a/account-proxy/server.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import process from 'node:process';
-
-import dotenv from 'dotenv';
-import Fastify from 'fastify';
-import cors from '@fastify/cors';
-import rateLimit from '@fastify/rate-limit';
-
-import proxyController from './controllers/proxyController.js';
-
-dotenv.config();
-
-const GITHUB_URL = 'https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass';
-
-// Limit requests to 10 per 30s for a single IP
-const RATE_LIMIT_MAX = 10;
-const RATE_LIMIT_TIME_WINDOW = 30000;
-
-const app = Fastify({
- trustProxy: process.env.ENABLE_TRUST_PROXY === '1',
-});
-
-// Enable CORS
-app.register(cors, { origin: true });
-
-// Enable rate limit
-app.register(rateLimit, {
- max: RATE_LIMIT_MAX,
- timeWindow: RATE_LIMIT_TIME_WINDOW,
-});
-
-// Routes
-app.get('/', (_, res) => res.redirect(GITHUB_URL));
-app.get('/getPlayer', proxyController.getPlayer);
-app.get('/getNext', proxyController.getNext);
-
-if (process.env.ENABLE_STATS === '1') {
- const { default: statsController } = await import('./controllers/statsController.js');
- const { default: stats } = await import('./lib/stats.js');
- stats.init();
- app.get('/getStats', statsController.getStats);
-}
-
-app.listen({ port: process.env.PORT }, (err, address) => {
- if (err) {
- console.error(err);
- process.exit(1);
- }
- console.info(`Server listening at ${address}`);
-});
diff --git a/package.json b/package.json
index a3b2978..427e37d 100644
--- a/package.json
+++ b/package.json
@@ -17,14 +17,10 @@
"files": [
"dist/Simple-YouTube-Age-Restriction-Bypass.user.js"
],
- "workspaces": [
- "account-proxy"
- ],
"scripts": {
"build": "node --no-warnings scripts/build.js",
"format": "dprint fmt",
- "check-format": "dprint check",
- "proxy:start": "npm start --workspaces account-proxy"
+ "check-format": "dprint check"
},
"devDependencies": {
"@babel/preset-env": "^7.26.0",
diff --git a/scripts/build.js b/scripts/build.js
index 03ee9d9..41054f4 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -1,5 +1,5 @@
import child_process from 'node:child_process';
-import fs from 'node:fs';
+import fs, { readFileSync } from 'node:fs';
import path from 'node:path';
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
@@ -7,24 +7,55 @@ import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { rollup } from 'rollup';
-import html from 'rollup-plugin-html';
import archiver from 'archiver';
-import pkg from '../package.json' assert { type: 'json' };
-import manifest from '../src/extension/mv3/manifest.json' assert { type: 'json' };
+const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
+const manifest = JSON.parse(readFileSync('./src/extension/mv3/manifest.json', 'utf8'));
const OUT_PATH = 'dist';
-const USERSCRIPT_OUT_DIR = OUT_PATH;
-const USERSCRIPT_OUT_NAME = 'Simple-YouTube-Age-Restriction-Bypass.user.js';
-
const WEB_EXTENSION_OUT_DIR = `${OUT_PATH}/extension`;
const WEB_EXTENSION_OUT_MAIN_SCRIPT_NAME = manifest.content_scripts[0].js[0];
const WEB_EXTENSION_OUT_WEB_SCRIPT_NAME = manifest.web_accessible_resources[0].resources[0];
const WEB_EXTENSION_OUT_MV2_ZIP_NAME = 'extension_mv2_firefox.zip';
const WEB_EXTENSION_OUT_MV3_ZIP_NAME = 'extension_mv3_chromium.zip';
+const USERSCRIPT_OUT_DIR = OUT_PATH;
+const USERSCRIPT_OUT_NAME = 'Simple-YouTube-Age-Restriction-Bypass.user.js';
+const USERSCRIPT_CONFIG = `
+// ==UserScript==
+// @name Simple YouTube Age Restriction Bypass
+// @description Watch age restricted videos on YouTube without login and without age verification 😎
+// @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen 😎
+// @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge 😎
+// @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età 😎
+// @icon https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/v2.5.4/src/extension/icon/icon_64.png
+// @version __BUILD_VERSION__
+// @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
+// @updateURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/main/dist/Simple-YouTube-Age-Restriction-Bypass.user.js
+// @license MIT
+// @match https://www.youtube.com/*
+// @match https://www.youtube-nocookie.com/*
+// @match https://m.youtube.com/*
+// @match https://music.youtube.com/*
+// @grant none
+// @run-at document-start
+// @compatible chrome
+// @compatible firefox
+// @compatible opera
+// @compatible edge
+// @compatible safari
+// ==/UserScript==
+
+/*
+ This is a transpiled version to achieve a clean code base and better browser compatibility.
+ You can find the nicely readable source code at https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
+*/
+`;
+
console.time('\nTotal build time');
console.time('Cleaning /dist');
@@ -54,9 +85,7 @@ function cleanOutput() {
}
async function buildUserscript() {
- function userscript(path) {
- const config = fs.readFileSync(path, 'utf8');
-
+ function userscript() {
const iife = (() => {
const [top, bottom] = (() => {
(function iife(ranOnce) {
@@ -76,7 +105,7 @@ async function buildUserscript() {
return {
name: 'userscript',
- banner: config + iife.top,
+ banner: USERSCRIPT_CONFIG + iife.top,
footer: iife.bottom,
};
}
@@ -88,10 +117,9 @@ async function buildUserscript() {
format: 'esm',
},
plugins: [
- html(),
nodeResolve(),
commonjs(),
- userscript('userscript.config.js'),
+ userscript(),
replace({
__BUILD_TARGET__: `'USERSCRIPT'`,
__BUILD_VERSION__: pkg.version,
@@ -173,7 +201,6 @@ async function buildWebExtension() {
format: 'iife',
},
plugins: [
- html(),
nodeResolve(),
commonjs(),
replace({ __BUILD_TARGET__: `'WEB_EXTENSION'`, preventAssignment: true }),
diff --git a/src/components/confirmation.js b/src/components/confirmation.js
deleted file mode 100644
index fff778b..0000000
--- a/src/components/confirmation.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Config from '../config';
-import { isConfirmed, isEmbed, setUrlParams } from '../utils';
-import { addButton, removeButton } from './errorScreen';
-
-const confirmationButtonId = 'confirmButton';
-const confirmationButtonText = 'Click to unlock';
-
-export function isConfirmationRequired() {
- return !isConfirmed && isEmbed && Config.ENABLE_UNLOCK_CONFIRMATION_EMBED;
-}
-
-export function requestConfirmation() {
- addButton(confirmationButtonId, confirmationButtonText, null, () => {
- removeButton(confirmationButtonId);
- confirm();
- });
-}
-
-function confirm() {
- setUrlParams({
- unlock_confirmed: 1,
- autoplay: 1,
- });
-}
diff --git a/src/components/endpoints/index.js b/src/components/endpoints/index.js
deleted file mode 100644
index b50e3b1..0000000
--- a/src/components/endpoints/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as innertube } from './innertube.js';
-export { default as proxy } from './proxy.js';
diff --git a/src/components/endpoints/innertube.js b/src/components/endpoints/innertube.js
deleted file mode 100644
index 2ad0a6b..0000000
--- a/src/components/endpoints/innertube.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Config from '../../config';
-import { getYtcfgValue, isUserLoggedIn } from '../../utils';
-import { nativeJSONParse } from '../interceptors/natives';
-import * as storage from '../storage';
-
-function getPlayer(payload, useAuth) {
- return sendInnertubeRequest('v1/player', payload, useAuth);
-}
-
-function getNext(payload, useAuth) {
- return sendInnertubeRequest('v1/next', payload, useAuth);
-}
-
-function sendInnertubeRequest(endpoint, payload, useAuth) {
- const xmlhttp = new XMLHttpRequest();
- xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);
-
- if (useAuth && isUserLoggedIn()) {
- xmlhttp.withCredentials = true;
- Config.GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
- xmlhttp.setRequestHeader(headerName, storage.get(headerName));
- });
- }
-
- xmlhttp.send(JSON.stringify(payload));
- return nativeJSONParse(xmlhttp.responseText);
-}
-
-export default {
- getPlayer,
- getNext,
-};
diff --git a/src/components/endpoints/proxy.js b/src/components/endpoints/proxy.js
deleted file mode 100644
index f613461..0000000
--- a/src/components/endpoints/proxy.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Config from '../../config';
-import { isEmbed, isMusic } from '../../utils';
-import * as logger from '../../utils/logger';
-import { nativeJSONParse } from '../interceptors/natives';
-
-let nextResponseCache = {};
-
-function getGoogleVideoUrl(originalUrl) {
- return Config.VIDEO_PROXY_SERVER_HOST + '/direct/' + btoa(originalUrl.toString());
-}
-
-function getPlayer(payload) {
- // Also request the /next response if a later /next request is likely.
- if (!nextResponseCache[payload.videoId] && !isMusic && !isEmbed) {
- payload.includeNext = 1;
- }
-
- return sendRequest('getPlayer', payload);
-}
-
-function getNext(payload) {
- // Next response already cached? => Return cached content
- if (nextResponseCache[payload.videoId]) {
- return nextResponseCache[payload.videoId];
- }
-
- return sendRequest('getNext', payload);
-}
-
-function sendRequest(endpoint, payload) {
- const queryParams = new URLSearchParams(payload);
- const proxyUrl = `${Config.ACCOUNT_PROXY_SERVER_HOST}/${endpoint}?${queryParams}&client=js`;
-
- try {
- const xmlhttp = new XMLHttpRequest();
- xmlhttp.open('GET', proxyUrl, false);
- xmlhttp.send(null);
-
- const proxyResponse = nativeJSONParse(xmlhttp.responseText);
-
- // Mark request as 'proxied'
- proxyResponse.proxied = true;
-
- // Put included /next response in the cache
- if (proxyResponse.nextResponse) {
- nextResponseCache[payload.videoId] = proxyResponse.nextResponse;
- delete proxyResponse.nextResponse;
- }
-
- return proxyResponse;
- } catch (err) {
- logger.error(err, 'Proxy API Error');
- return { errorMessage: 'Proxy Connection failed' };
- }
-}
-
-export default {
- getPlayer,
- getNext,
- getGoogleVideoUrl,
-};
diff --git a/src/components/errorScreen/index.js b/src/components/errorScreen/index.js
deleted file mode 100644
index d2ef2e5..0000000
--- a/src/components/errorScreen/index.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { createElement, waitForElement } from '../../utils';
-import buttonTemplate from './templates/button.html';
-
-let buttons = {};
-
-export async function addButton(id, text, backgroundColor, onClick) {
- const errorScreenElement = await waitForElement('.ytp-error', 2000);
- const buttonElement = createElement('div', { class: 'button-container', innerHTML: buttonTemplate });
- buttonElement.getElementsByClassName('button-text')[0].innerText = text;
-
- if (backgroundColor) {
- buttonElement.querySelector(':scope > div').style['background-color'] = backgroundColor;
- }
-
- if (typeof onClick === 'function') {
- buttonElement.addEventListener('click', onClick);
- }
-
- // Button already attached?
- if (buttons[id] && buttons[id].isConnected) {
- return;
- }
-
- buttons[id] = buttonElement;
- errorScreenElement.append(buttonElement);
-}
-
-export function removeButton(id) {
- if (buttons[id] && buttons[id].isConnected) {
- buttons[id].remove();
- }
-}
diff --git a/src/components/errorScreen/templates/button.html b/src/components/errorScreen/templates/button.html
deleted file mode 100644
index 2ab9241..0000000
--- a/src/components/errorScreen/templates/button.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/innertube.js b/src/components/innertube.js
new file mode 100644
index 0000000..59325ae
--- /dev/null
+++ b/src/components/innertube.js
@@ -0,0 +1,27 @@
+import { getYtcfgValue, GOOGLE_AUTH_HEADER_NAMES, isUserLoggedIn, nativeJSONParse } from '../utils.js';
+
+const getPlayer = sendInnertubeRequest.bind(null, 'v1/player');
+const getNext = sendInnertubeRequest.bind(null, 'v1/next');
+
+function sendInnertubeRequest(endpoint, payload, useAuth) {
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);
+
+ if (useAuth && isUserLoggedIn()) {
+ xmlhttp.withCredentials = true;
+ GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
+ const value = localStorage.getItem('SYARB_' + headerName);
+ if (value) {
+ xmlhttp.setRequestHeader(headerName, JSON.parse(value));
+ }
+ });
+ }
+
+ xmlhttp.send(JSON.stringify(payload));
+ return nativeJSONParse(xmlhttp.responseText);
+}
+
+export default {
+ getPlayer,
+ getNext,
+};
diff --git a/src/components/inspectors/googlevideo.js b/src/components/inspectors/googlevideo.js
deleted file mode 100644
index 5cc70b7..0000000
--- a/src/components/inspectors/googlevideo.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export function isGoogleVideoUrl(url) {
- return url.host.includes('.googlevideo.com');
-}
-
-export function isGoogleVideoUnlockRequired(googleVideoUrl, lastProxiedGoogleVideoId) {
- const urlParams = new URLSearchParams(googleVideoUrl.search);
- const hasGcrFlag = urlParams.get('gcr');
- const wasUnlockedByAccountProxy = urlParams.get('id') === lastProxiedGoogleVideoId;
-
- return hasGcrFlag && wasUnlockedByAccountProxy;
-}
diff --git a/src/components/inspectors/index.js b/src/components/inspectors/index.js
deleted file mode 100644
index cc46f15..0000000
--- a/src/components/inspectors/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export * as googlevideo from './googlevideo';
-export * as next from './next';
-export * as player from './player';
-export * as search from './search';
diff --git a/src/components/inspectors/next.js b/src/components/inspectors/next.js
deleted file mode 100644
index 12c44b2..0000000
--- a/src/components/inspectors/next.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { isDesktop } from '../../utils';
-
-export function isWatchNextObject(parsedData) {
- if (!parsedData?.contents || !parsedData?.currentVideoEndpoint?.watchEndpoint?.videoId) return false;
- return !!parsedData.contents.twoColumnWatchNextResults || !!parsedData.contents.singleColumnWatchNextResults;
-}
-
-export function isWatchNextSidebarEmpty(parsedData) {
- if (isDesktop) {
- // WEB response layout
- const result = parsedData.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results;
- return !result;
- }
-
- // MWEB response layout
- const content = parsedData.contents?.singleColumnWatchNextResults?.results?.results?.contents;
- const result = content?.find((e) => e.itemSectionRenderer?.targetId === 'watch-next-feed')?.itemSectionRenderer;
- return typeof result !== 'object';
-}
diff --git a/src/components/inspectors/player.js b/src/components/inspectors/player.js
deleted file mode 100644
index fae7da7..0000000
--- a/src/components/inspectors/player.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Config from '../../config';
-import { isEmbed } from '../../utils';
-
-export function isPlayerObject(parsedData) {
- return parsedData?.videoDetails && parsedData?.playabilityStatus;
-}
-
-export function isEmbeddedPlayerObject(parsedData) {
- return typeof parsedData?.previewPlayabilityStatus === 'object';
-}
-
-export function isAgeRestricted(playabilityStatus) {
- if (!playabilityStatus?.status) return false;
- if (playabilityStatus.desktopLegacyAgeGateReason) return true;
- if (Config.UNLOCKABLE_PLAYABILITY_STATUSES.includes(playabilityStatus.status)) return true;
-
- // Fix to detect age restrictions on embed player
- // see https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/85#issuecomment-946853553
- return (
- isEmbed
- && playabilityStatus.errorScreen?.playerErrorMessageRenderer?.reason?.runs?.find((x) => x.navigationEndpoint)?.navigationEndpoint?.urlEndpoint?.url?.includes('/2802167')
- );
-}
-
-export function isUnplayable(playabilityStatus) {
- return playabilityStatus?.status === 'UNPLAYABLE';
-}
diff --git a/src/components/inspectors/search.js b/src/components/inspectors/search.js
deleted file mode 100644
index 21da231..0000000
--- a/src/components/inspectors/search.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export function isSearchResult(parsedData) {
- return (
- typeof parsedData?.contents?.twoColumnSearchResultsRenderer === 'object' // Desktop initial results
- || parsedData?.contents?.sectionListRenderer?.targetId === 'search-feed' // Mobile initial results
- || parsedData?.onResponseReceivedCommands?.find((x) => x.appendContinuationItemsAction)?.appendContinuationItemsAction?.targetId === 'search-feed' // Desktop & Mobile scroll continuation
- );
-}
diff --git a/src/components/interceptors/generic.js b/src/components/interceptors/generic.js
deleted file mode 100644
index ca92c06..0000000
--- a/src/components/interceptors/generic.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function attach(obj, prop, onCall) {
- if (!obj || typeof obj[prop] !== 'function') {
- return;
- }
-
- let original = obj[prop];
-
- obj[prop] = function() {
- try {
- onCall(arguments);
- } catch {}
- original.apply(this, arguments);
- };
-}
diff --git a/src/components/interceptors/index.js b/src/components/interceptors/index.js
deleted file mode 100644
index 3da07b4..0000000
--- a/src/components/interceptors/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export { default as attachGenericInterceptor } from './generic';
-export { default as attachInitialDataInterceptor } from './initialData';
-export { default as attachJsonInterceptor } from './json';
-export { default as attachRequestInterceptor } from './request';
-export { default as attachXhrOpenInterceptor } from './xhrOpen';
diff --git a/src/components/interceptors/initialData.js b/src/components/interceptors/initialData.js
deleted file mode 100644
index 6e030c9..0000000
--- a/src/components/interceptors/initialData.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createDeepCopy } from '../../utils';
-import { isObject } from '../../utils';
-import * as logger from '../../utils/logger';
-
-/**
- * And here we deal with YouTube's crappy initial data (present in page source) and the problems that occur when intercepting that data.
- * YouTube has some protections in place that make it difficult to intercept and modify the global ytInitialPlayerResponse variable.
- * The easiest way would be to set a descriptor on that variable to change the value directly on declaration.
- * But some adblockers define their own descriptors on the ytInitialPlayerResponse variable, which makes it hard to register another descriptor on it.
- * As a workaround only the relevant playerResponse property of the ytInitialPlayerResponse variable will be intercepted.
- * This is achieved by defining a descriptor on the object prototype for that property, which affects any object with a `playerResponse` property.
- */
-export default function attach(onInitialData) {
- interceptObjectProperty('playerResponse', (obj, playerResponse) => {
- logger.info(`playerResponse property set, contains sidebar: ${!!obj.response}`);
-
- // The same object also contains the sidebar data and video description
- if (isObject(obj.response)) onInitialData(obj.response);
-
- // If the script is executed too late and the bootstrap data has already been processed,
- // a reload of the player can be forced by creating a deep copy of the object.
- // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
- playerResponse.unlocked = false;
- onInitialData(playerResponse);
- return playerResponse.unlocked ? createDeepCopy(playerResponse) : playerResponse;
- });
-
- // The global `ytInitialData` variable can be modified on the fly.
- // It contains search results, sidebar data and meta information
- // Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
- window.addEventListener('DOMContentLoaded', () => {
- if (isObject(window.ytInitialData)) {
- onInitialData(window.ytInitialData);
- }
- });
-}
-
-function interceptObjectProperty(prop, onSet) {
- // Allow other userscripts to decorate this descriptor, if they do something similar
- const dataKey = '__SYARB_' + prop;
- const { get: getter, set: setter } = Object.getOwnPropertyDescriptor(Object.prototype, prop) ?? {
- set(value) {
- this[dataKey] = value;
- },
- get() {
- return this[dataKey];
- },
- };
-
- // Intercept the given property on any object
- // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
- Object.defineProperty(Object.prototype, prop, {
- set(value) {
- setter.call(this, isObject(value) ? onSet(this, value) : value);
- },
- get() {
- return getter.call(this);
- },
- configurable: true,
- });
-}
diff --git a/src/components/interceptors/json.js b/src/components/interceptors/json.js
deleted file mode 100644
index f077394..0000000
--- a/src/components/interceptors/json.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { isObject } from '../../utils';
-import { nativeJSONParse } from './natives';
-
-// Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
-export default function attach(onJsonDataReceived) {
- window.JSON.parse = function() {
- const data = nativeJSONParse.apply(this, arguments);
- return isObject(data) ? onJsonDataReceived(data) : data;
- };
-}
diff --git a/src/components/interceptors/natives.js b/src/components/interceptors/natives.js
deleted file mode 100644
index 88cb33c..0000000
--- a/src/components/interceptors/natives.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const nativeJSONParse = window.JSON.parse;
-export const nativeXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;
diff --git a/src/components/interceptors/request.js b/src/components/interceptors/request.js
deleted file mode 100644
index d2b8815..0000000
--- a/src/components/interceptors/request.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as logger from '../../utils/logger';
-
-export default function attach(onRequestCreate) {
- if (typeof window.Request !== 'function') {
- return;
- }
-
- window.Request = new Proxy(window.Request, {
- construct(target, args) {
- let [url, options] = args;
- try {
- if (typeof url === 'string') {
- if (url.indexOf('/') === 0) {
- url = window.location.origin + url;
- }
-
- if (url.indexOf('https://') !== -1) {
- const modifiedUrl = onRequestCreate(url, options);
-
- if (modifiedUrl) {
- args[0] = modifiedUrl;
- }
- }
- }
- } catch (err) {
- logger.error(err, `Failed to intercept Request()`);
- }
-
- return Reflect.construct(target, args);
- },
- });
-}
diff --git a/src/components/interceptors/xhrOpen.js b/src/components/interceptors/xhrOpen.js
deleted file mode 100644
index cf54615..0000000
--- a/src/components/interceptors/xhrOpen.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as logger from '../../utils/logger';
-import { nativeXMLHttpRequestOpen } from './natives';
-
-export default function attach(onXhrOpenCalled) {
- XMLHttpRequest.prototype.open = function(...args) {
- let [method, url] = args;
- try {
- if (typeof url === 'string') {
- if (url.indexOf('/') === 0) {
- url = window.location.origin + url;
- }
-
- if (url.indexOf('https://') !== -1) {
- const modifiedUrl = onXhrOpenCalled(method, url, this);
-
- if (modifiedUrl) {
- args[1] = modifiedUrl;
- }
- }
- }
- } catch (err) {
- logger.error(err, `Failed to intercept XMLHttpRequest.open()`);
- }
-
- nativeXMLHttpRequestOpen.apply(this, args);
- };
-}
diff --git a/src/components/requestPreprocessor.js b/src/components/requestPreprocessor.js
deleted file mode 100644
index 41ade8a..0000000
--- a/src/components/requestPreprocessor.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import Config from '../config';
-import { isObject } from '../utils';
-import { proxy } from './endpoints';
-import * as inspectors from './inspectors';
-import * as interceptors from './interceptors';
-import * as storage from './storage';
-import * as unlocker from './unlocker';
-
-/**
- * Handles XMLHttpRequests and
- * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
- * - Store auth headers for the authentication of further unlock requests.
- * - Add "content check ok" flags to request bodys
- */
-export function handleXhrOpen(method, url, xhr) {
- const url_obj = new URL(url);
- let proxyUrl = unlockGoogleVideo(url_obj);
- if (proxyUrl) {
- // Exclude credentials from XMLHttpRequest
- Object.defineProperty(xhr, 'withCredentials', {
- set: () => {},
- get: () => false,
- });
- return proxyUrl.toString();
- }
-
- if (url_obj.pathname.indexOf('/youtubei/') === 0) {
- // Store auth headers in storage for further usage.
- interceptors.attachGenericInterceptor(xhr, 'setRequestHeader', ([headerName, headerValue]) => {
- if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
- storage.set(headerName, headerValue);
- }
- });
- }
-
- if (Config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
- // Add content check flags to player and next request (this will skip content warnings)
- interceptors.attachGenericInterceptor(xhr, 'send', (args) => {
- if (typeof args[0] === 'string') {
- args[0] = setContentCheckOk(args[0]);
- }
- });
- }
-}
-
-/**
- * Handles Fetch requests and
- * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
- * - Store auth headers for the authentication of further unlock requests.
- * - Add "content check ok" flags to request bodys
- */
-export function handleFetchRequest(url, requestOptions) {
- const url_obj = new URL(url);
- const newGoogleVideoUrl = unlockGoogleVideo(url_obj);
- if (newGoogleVideoUrl) {
- // Exclude credentials from Fetch Request
- if (requestOptions.credentials) {
- requestOptions.credentials = 'omit';
- }
- return newGoogleVideoUrl.toString();
- }
-
- if (url_obj.pathname.indexOf('/youtubei/') === 0 && isObject(requestOptions.headers)) {
- // Store auth headers in authStorage for further usage.
- for (let headerName in requestOptions.headers) {
- if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
- storage.set(headerName, requestOptions.headers[headerName]);
- }
- }
- }
-
- if (Config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
- // Add content check flags to player and next request (this will skip content warnings)
- requestOptions.body = setContentCheckOk(requestOptions.body);
- }
-}
-
-/**
- * If the account proxy was used to retrieve the video info, the following applies:
- * some video files (mostly music videos) can only be accessed from IPs in the same country as the innertube api request (/youtubei/v1/player) was made.
- * to get around this, the googlevideo URL will be replaced with a web-proxy URL in the same country (US).
- * this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url...
- * @returns The rewitten url (if a proxy is required)
- */
-function unlockGoogleVideo(url) {
- if (Config.VIDEO_PROXY_SERVER_HOST && inspectors.googlevideo.isGoogleVideoUrl(url)) {
- if (inspectors.googlevideo.isGoogleVideoUnlockRequired(url, unlocker.getLastProxiedGoogleVideoId())) {
- return proxy.getGoogleVideoUrl(url);
- }
- }
-}
-
-/**
- * Adds `contentCheckOk` and `racyCheckOk` to the given json data (if the data contains a video id)
- * @returns {string} The modified json
- */
-function setContentCheckOk(bodyJson) {
- try {
- let parsedBody = JSON.parse(bodyJson);
- if (parsedBody.videoId) {
- parsedBody.contentCheckOk = true;
- parsedBody.racyCheckOk = true;
- return JSON.stringify(parsedBody);
- }
- } catch {}
- return bodyJson;
-}
diff --git a/src/components/storage.js b/src/components/storage.js
deleted file mode 100644
index b4390ca..0000000
--- a/src/components/storage.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const localStoragePrefix = 'SYARB_';
-
-export function set(key, value) {
- localStorage.setItem(localStoragePrefix + key, JSON.stringify(value));
-}
-
-export function get(key) {
- try {
- return JSON.parse(localStorage.getItem(localStoragePrefix + key));
- } catch {
- return null;
- }
-}
diff --git a/src/components/strategies/index.js b/src/components/strategies/index.js
deleted file mode 100644
index 96b96b4..0000000
--- a/src/components/strategies/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as getNextUnlockStrategies } from './next.js';
-export { default as getPlayerUnlockStrategies } from './player.js';
diff --git a/src/components/strategies/next.js b/src/components/strategies/next.js
deleted file mode 100644
index 6343d9d..0000000
--- a/src/components/strategies/next.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { getYtcfgValue, isConfirmed, isEmbed } from '../../utils';
-import { innertube, proxy } from '../endpoints';
-
-export default function getUnlockStrategies(videoId, lastPlayerUnlockReason) {
- const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
- const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
- const hl = getYtcfgValue('HL');
- const userInterfaceTheme = getYtcfgValue('INNERTUBE_CONTEXT').client.userInterfaceTheme
- ?? (document.documentElement.hasAttribute('dark') ? 'USER_INTERFACE_THEME_DARK' : 'USER_INTERFACE_THEME_LIGHT');
-
- return [
- /**
- * Retrieve the sidebar and video description by just adding `racyCheckOk` and `contentCheckOk` params
- * This strategy can be used to bypass content warnings
- */
- {
- name: 'Content Warning Bypass',
- skip: !lastPlayerUnlockReason || !lastPlayerUnlockReason.includes('CHECK_REQUIRED'),
- optionalAuth: true,
- payload: {
- context: {
- client: {
- clientName,
- clientVersion,
- hl,
- userInterfaceTheme,
- },
- },
- videoId,
- racyCheckOk: true,
- contentCheckOk: true,
- },
- endpoint: innertube,
- },
- /**
- * Retrieve the sidebar and video description from an account proxy server.
- * Session cookies of an age-verified Google account are stored on server side.
- * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
- */
- {
- name: 'Account Proxy',
- payload: {
- videoId,
- clientName,
- clientVersion,
- hl,
- userInterfaceTheme,
- isEmbed: +isEmbed,
- isConfirmed: +isConfirmed,
- },
- endpoint: proxy,
- },
- ];
-}
diff --git a/src/components/strategies/player.js b/src/components/strategies/player.js
deleted file mode 100644
index 42ae597..0000000
--- a/src/components/strategies/player.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import { getCurrentVideoStartTime, getSignatureTimestamp, getYtcfgValue, isConfirmed, isEmbed } from '../../utils';
-import { innertube, proxy } from '../endpoints';
-
-export default function getUnlockStrategies(videoId, reason) {
- const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
- const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
- const signatureTimestamp = getSignatureTimestamp();
- const startTimeSecs = getCurrentVideoStartTime(videoId);
- const hl = getYtcfgValue('HL');
-
- return [
- /**
- * Retrieve the video info by just adding `racyCheckOk` and `contentCheckOk` params
- * This strategy can be used to bypass content warnings
- */
- {
- name: 'Content Warning Bypass',
- skip: !reason || !reason.includes('CHECK_REQUIRED'),
- optionalAuth: true,
- payload: {
- context: {
- client: {
- clientName: clientName,
- clientVersion: clientVersion,
- hl,
- },
- },
- playbackContext: {
- contentPlaybackContext: {
- signatureTimestamp,
- },
- },
- videoId,
- startTimeSecs,
- racyCheckOk: true,
- contentCheckOk: true,
- },
- endpoint: innertube,
- },
- /**
- * Retrieve the video info by using the TVHTML5 Embedded client
- * This client has no age restrictions in place (2022-03-28)
- * See https://github.com/zerodytrash/YouTube-Internal-Clients
- */
- {
- name: 'TV Embedded Player',
- requiresAuth: false,
- payload: {
- context: {
- client: {
- clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
- clientVersion: '2.0',
- clientScreen: 'WATCH',
- hl,
- },
- thirdParty: {
- embedUrl: 'https://www.youtube.com/',
- },
- },
- playbackContext: {
- contentPlaybackContext: {
- signatureTimestamp,
- },
- },
- videoId,
- startTimeSecs,
- racyCheckOk: true,
- contentCheckOk: true,
- },
- endpoint: innertube,
- },
- /**
- * Retrieve the video info by using the WEB_CREATOR client in combination with user authentication
- * Requires that the user is logged in. Can bypass the tightened age verification in the EU.
- * See https://github.com/yt-dlp/yt-dlp/pull/600
- */
- {
- name: 'Creator + Auth',
- requiresAuth: true,
- payload: {
- context: {
- client: {
- clientName: 'WEB_CREATOR',
- clientVersion: '1.20210909.07.00',
- hl,
- },
- },
- playbackContext: {
- contentPlaybackContext: {
- signatureTimestamp,
- },
- },
- videoId,
- startTimeSecs,
- racyCheckOk: true,
- contentCheckOk: true,
- },
- endpoint: innertube,
- },
- /**
- * Retrieve the video info from an account proxy server.
- * Session cookies of an age-verified Google account are stored on server side.
- * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
- */
- {
- name: 'Account Proxy',
- payload: {
- videoId,
- reason,
- clientName,
- clientVersion,
- signatureTimestamp,
- startTimeSecs,
- hl,
- isEmbed: +isEmbed,
- isConfirmed: +isConfirmed,
- },
- endpoint: proxy,
- },
- ];
-}
diff --git a/src/components/thumbnailFix.js b/src/components/thumbnailFix.js
index 3463143..c6a44ec 100644
--- a/src/components/thumbnailFix.js
+++ b/src/components/thumbnailFix.js
@@ -1,9 +1,22 @@
-import Config from '../config';
-import { findNestedObjectsByAttributeNames } from '../utils';
-import * as logger from '../utils/logger';
+import * as logger from '../logger.js';
+
+/**
+ * The SQP parameter length is different for blurred thumbnails.
+ * They contain much less information, than normal thumbnails.
+ * The thumbnail SQPs tend to have a long and a short version.
+ */
+const BLURRED_THUMBNAIL_SQP_LENGTHS = [
+ 32, // Mobile (SHORT)
+ 48, // Desktop Playlist (SHORT)
+ 56, // Desktop (SHORT)
+ 68, // Mobile (LONG)
+ 72, // Mobile Shorts
+ 84, // Desktop Playlist (LONG)
+ 88, // Desktop (LONG)
+];
export function processThumbnails(responseObject) {
- const thumbnails = findNestedObjectsByAttributeNames(responseObject, ['url', 'height']);
+ const thumbnails = findObjectsByInnerKeys(responseObject, ['url', 'height']);
let blurredThumbnailCount = 0;
@@ -25,7 +38,28 @@ function isThumbnailBlurred(thumbnail) {
}
const SQPLength = new URL(thumbnail.url).searchParams.get('sqp').length;
- const isBlurred = Config.BLURRED_THUMBNAIL_SQP_LENGTHS.includes(SQPLength);
+ const isBlurred = BLURRED_THUMBNAIL_SQP_LENGTHS.includes(SQPLength);
return isBlurred;
}
+
+function findObjectsByInnerKeys(object, keys) {
+ const result = [];
+ const stack = [object];
+
+ for (const obj of stack) {
+ // Check current object in the stack for keys
+ if (keys.every((key) => typeof obj[key] !== 'undefined')) {
+ result.push(obj);
+ }
+
+ // Put nested objects in the stack
+ for (const key in obj) {
+ if (obj[key] && typeof object[key] === 'object') {
+ stack.push(obj[key]);
+ }
+ }
+ }
+
+ return result;
+}
diff --git a/src/components/toast/templates/desktop.html b/src/components/toast/templates/desktop.html
deleted file mode 100644
index ebb067f..0000000
--- a/src/components/toast/templates/desktop.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/components/toast/templates/mobile.html b/src/components/toast/templates/mobile.html
deleted file mode 100644
index a6ccfa1..0000000
--- a/src/components/toast/templates/mobile.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/src/components/ui/errorScreen.js b/src/components/ui/errorScreen.js
new file mode 100644
index 0000000..e758ee1
--- /dev/null
+++ b/src/components/ui/errorScreen.js
@@ -0,0 +1,48 @@
+import * as config from '../../config.js';
+import { createElement, isConfirmed, isEmbed, waitForElement } from '../../utils.js';
+
+const confirmationButtonId = 'confirmButton';
+const confirmationButtonText = 'Click to unlock';
+
+const buttons = {};
+
+const buttonTemplate = `
+
+`;
+
+export function isConfirmationRequired() {
+ return !isConfirmed && isEmbed && config.ENABLE_UNLOCK_CONFIRMATION_EMBED;
+}
+
+export async function requestConfirmation() {
+ const errorScreenElement = await waitForElement('.ytp-error', 2000);
+ const buttonElement = createElement('div', { class: 'button-container', innerHTML: buttonTemplate });
+ buttonElement.getElementsByClassName('button-text')[0].innerText = confirmationButtonText;
+ buttonElement.addEventListener('click', () => {
+ removeButton(confirmationButtonId);
+ confirm();
+ });
+
+ // Button already attached?
+ if (buttons[confirmationButtonId] && buttons[confirmationButtonId].isConnected) {
+ return;
+ }
+
+ buttons[confirmationButtonId] = buttonElement;
+ errorScreenElement.append(buttonElement);
+}
+
+function confirm() {
+ const urlParams = new URLSearchParams(window.location.search);
+ urlParams.set('unlock_confirmed', '1');
+ urlParams.set('autoplay', '1');
+ location.search = urlParams.toString();
+}
+
+function removeButton(id) {
+ if (buttons[id] && buttons[id].isConnected) {
+ buttons[id].remove();
+ }
+}
diff --git a/src/components/toast/index.js b/src/components/ui/toast.js
similarity index 76%
rename from src/components/toast/index.js
rename to src/components/ui/toast.js
index 90ebc9e..3b27564 100644
--- a/src/components/toast/index.js
+++ b/src/components/ui/toast.js
@@ -1,8 +1,15 @@
-import Config from '../../config';
-import { createElement, isDesktop, isEmbed, isMusic, pageLoaded } from '../../utils';
+import * as config from '../../config.js';
+import { createElement, isDesktop, isEmbed, isMusic, pageLoaded } from '../../utils.js';
-import tDesktop from './templates/desktop.html';
-import tMobile from './templates/mobile.html';
+const tDesktop = ``;
+
+const tMobile = `
+
+
+
+
+
+`;
const template = isDesktop ? tDesktop : tMobile;
@@ -26,7 +33,7 @@ if (!isDesktop) {
}
async function show(message, duration = 5) {
- if (!Config.ENABLE_UNLOCK_NOTIFICATION) return;
+ if (!config.ENABLE_UNLOCK_NOTIFICATION) return;
if (isEmbed) return;
await pageLoaded();
diff --git a/src/components/unlocker/index.js b/src/components/unlocker/index.js
deleted file mode 100644
index 4836e1b..0000000
--- a/src/components/unlocker/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as unlockNextResponse } from './next.js';
-export { default as unlockPlayerResponse } from './player.js';
-
-export { getLastProxiedGoogleVideoId, lastPlayerUnlockReason, lastPlayerUnlockVideoId } from './player.js';
diff --git a/src/components/unlocker/next.js b/src/components/unlocker/next.js
index 932ba67..423271c 100644
--- a/src/components/unlocker/next.js
+++ b/src/components/unlocker/next.js
@@ -1,13 +1,14 @@
-import { createDeepCopy, isDesktop } from '../../utils';
-import * as logger from '../../utils/logger';
-import { next as nextInspector } from '../inspectors';
-import { getNextUnlockStrategies } from '../strategies';
-import { lastPlayerUnlockReason, lastPlayerUnlockVideoId } from './player';
+import * as logger from '../../logger.js';
+import { createDeepCopy, getYtcfgValue, isDesktop } from '../../utils.js';
+import innertube from '../innertube.js';
+import { lastPlayerUnlockReason, lastPlayerUnlockVideoId } from './player.js';
let cachedNextResponse = {};
-export default function unlockResponse(originalNextResponse) {
- const videoId = originalNextResponse.currentVideoEndpoint.watchEndpoint.videoId;
+export function unlockResponse(ytData) {
+ const response = ytData.response ?? ytData;
+
+ const videoId = response.currentVideoEndpoint.watchEndpoint.videoId;
if (!videoId) {
throw new Error(`Missing videoId in nextResponse`);
@@ -21,41 +22,39 @@ export default function unlockResponse(originalNextResponse) {
const unlockedNextResponse = getUnlockedNextResponse(videoId);
// check if the sidebar of the unlocked response is still empty
- if (nextInspector.isWatchNextSidebarEmpty(unlockedNextResponse)) {
+ if (isWatchNextSidebarEmpty(unlockedNextResponse)) {
throw new Error(`Sidebar Unlock Failed`);
}
// Transfer some parts of the unlocked response to the original response
- mergeNextResponse(originalNextResponse, unlockedNextResponse);
+ mergeNextResponse(response, unlockedNextResponse);
}
function getUnlockedNextResponse(videoId) {
// Check if response is cached
if (cachedNextResponse.videoId === videoId) return createDeepCopy(cachedNextResponse);
- const unlockStrategies = getNextUnlockStrategies(videoId, lastPlayerUnlockReason);
+ const unlockStrategies = getUnlockStrategies(videoId, lastPlayerUnlockReason);
let unlockedNextResponse = {};
- // Try every strategy until one of them works
- unlockStrategies.every((strategy, index) => {
- if (strategy.skip) return true;
+ for (const strategy of unlockStrategies) {
+ if (strategy.skip) continue;
- logger.info(`Trying Next Unlock Method #${index + 1} (${strategy.name})`);
+ logger.info(`Trying Next Unlock Method ${strategy.name}`);
try {
unlockedNextResponse = strategy.endpoint.getNext(strategy.payload, strategy.optionalAuth);
} catch (err) {
- logger.error(err, `Next Unlock Method ${index + 1} failed with exception`);
+ logger.error(`Next unlock Method "${strategy.name}" failed with exception:`, err);
}
- return nextInspector.isWatchNextSidebarEmpty(unlockedNextResponse);
- });
-
- // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
- cachedNextResponse = { videoId, ...createDeepCopy(unlockedNextResponse) };
-
- return unlockedNextResponse;
+ if (!isWatchNextSidebarEmpty(unlockedNextResponse)) {
+ // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
+ cachedNextResponse = { videoId, ...createDeepCopy(unlockedNextResponse) };
+ return unlockedNextResponse;
+ }
+ }
}
function mergeNextResponse(originalNextResponse, unlockedNextResponse) {
@@ -100,3 +99,58 @@ function mergeNextResponse(originalNextResponse, unlockedNextResponse) {
originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer = unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer;
}
}
+
+function getUnlockStrategies(videoId, lastPlayerUnlockReason) {
+ const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
+ const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
+ const hl = getYtcfgValue('HL');
+ const userInterfaceTheme = getYtcfgValue('INNERTUBE_CONTEXT').client.userInterfaceTheme
+ ?? (document.documentElement.hasAttribute('dark') ? 'USER_INTERFACE_THEME_DARK' : 'USER_INTERFACE_THEME_LIGHT');
+
+ return [
+ /**
+ * Retrieve the sidebar and video description by just adding `racyCheckOk` and `contentCheckOk` params
+ * This strategy can be used to bypass content warnings
+ */
+ {
+ name: 'Content Warning Bypass',
+ skip: !lastPlayerUnlockReason || !lastPlayerUnlockReason.includes('CHECK_REQUIRED'),
+ optionalAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName,
+ clientVersion,
+ hl,
+ userInterfaceTheme,
+ },
+ },
+ videoId,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ ];
+}
+
+export function isWatchNextObject(ytData) {
+ const response = ytData.response ?? ytData;
+ if (!response?.contents || !response?.currentVideoEndpoint?.watchEndpoint?.videoId) return false;
+ return !!response.contents.twoColumnWatchNextResults || !!response.contents.singleColumnWatchNextResults;
+}
+
+export function isWatchNextSidebarEmpty(ytData) {
+ const response = ytData.response ?? ytData;
+
+ if (isDesktop) {
+ // WEB response layout
+ const result = response.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results;
+ return !result;
+ }
+
+ // MWEB response layout
+ const content = response.contents?.singleColumnWatchNextResults?.results?.results?.contents;
+ const result = content?.find((e) => e.itemSectionRenderer?.targetId === 'watch-next-feed')?.itemSectionRenderer;
+ return typeof result !== 'object';
+}
diff --git a/src/components/unlocker/player.js b/src/components/unlocker/player.js
index a1cae2a..0efcb46 100644
--- a/src/components/unlocker/player.js
+++ b/src/components/unlocker/player.js
@@ -1,15 +1,18 @@
-import Config from '../../config';
-import { createDeepCopy, getYtcfgValue, isUserLoggedIn } from '../../utils';
-import * as logger from '../../utils/logger';
-import { isConfirmationRequired, requestConfirmation } from '../confirmation';
-import { getPlayerUnlockStrategies } from '../strategies';
-import Toast from '../toast';
+import * as config from '../../config.js';
+import * as logger from '../../logger.js';
+import { createDeepCopy, getSignatureTimestamp, getYtcfgValue, isEmbed, isUserLoggedIn } from '../../utils.js';
+import innertube from '../innertube.js';
+import { isConfirmationRequired, requestConfirmation } from '../ui/errorScreen.js';
+import Toast from '../ui/toast.js';
const messagesMap = {
success: 'Age-restricted video successfully unlocked!',
fail: 'Unable to unlock this video 🙁 - More information in the developer console',
};
+const UNLOCKABLE_PLAYABILITY_STATUSES = ['AGE_VERIFICATION_REQUIRED', 'AGE_CHECK_REQUIRED', 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
+const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];
+
export let lastPlayerUnlockVideoId = null;
export let lastPlayerUnlockReason = null;
@@ -20,7 +23,7 @@ export function getLastProxiedGoogleVideoId() {
return lastProxiedGoogleVideoUrlParams?.get('id');
}
-export default function unlockResponse(playerResponse) {
+export function unlockResponse(playerResponse) {
// Check if the user has to confirm the unlock first
if (isConfirmationRequired()) {
logger.info('Unlock confirmation required.');
@@ -31,7 +34,7 @@ export default function unlockResponse(playerResponse) {
const videoId = playerResponse.videoDetails?.videoId || getYtcfgValue('PLAYER_VARS').video_id;
const reason = playerResponse.playabilityStatus?.status || playerResponse.previewPlayabilityStatus?.status;
- if (!Config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
+ if (!config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
logger.info(`SKIP_CONTENT_WARNINGS disabled and ${reason} status detected.`);
return;
}
@@ -41,26 +44,12 @@ export default function unlockResponse(playerResponse) {
const unlockedPlayerResponse = getUnlockedPlayerResponse(videoId, reason);
- // account proxy error?
- if (unlockedPlayerResponse.errorMessage) {
- Toast.show(`${messagesMap.fail} (ProxyError)`, 10);
- throw new Error(`Player Unlock Failed, Proxy Error Message: ${unlockedPlayerResponse.errorMessage}`);
- }
-
// check if the unlocked response isn't playable
- if (!Config.VALID_PLAYABILITY_STATUSES.includes(unlockedPlayerResponse.playabilityStatus?.status)) {
+ if (!VALID_PLAYABILITY_STATUSES.includes(unlockedPlayerResponse.playabilityStatus?.status)) {
Toast.show(`${messagesMap.fail} (PlayabilityError)`, 10);
throw new Error(`Player Unlock Failed, playabilityStatus: ${unlockedPlayerResponse.playabilityStatus?.status}`);
}
- // if the video info was retrieved via proxy, store the URL params from the url-attribute to detect later if the requested video file (googlevideo.com) need a proxy.
- if (unlockedPlayerResponse.proxied && unlockedPlayerResponse.streamingData?.adaptiveFormats) {
- const cipherText = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) => x.signatureCipher)?.signatureCipher;
- const videoUrl = cipherText ? new URLSearchParams(cipherText).get('url') : unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) => x.url)?.url;
-
- lastProxiedGoogleVideoUrlParams = videoUrl ? new URLSearchParams(new window.URL(videoUrl).search) : null;
- }
-
// Overwrite the embedded (preview) playabilityStatus with the unlocked one
if (playerResponse.previewPlayabilityStatus) {
playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;
@@ -69,33 +58,34 @@ export default function unlockResponse(playerResponse) {
// Transfer all unlocked properties to the original player response
Object.assign(playerResponse, unlockedPlayerResponse);
- playerResponse.unlocked = true;
-
Toast.show(messagesMap.success);
+
+ return true;
}
function getUnlockedPlayerResponse(videoId, reason) {
// Check if response is cached
if (cachedPlayerResponse.videoId === videoId) return createDeepCopy(cachedPlayerResponse);
- const unlockStrategies = getPlayerUnlockStrategies(videoId, reason);
+ const unlockStrategies = getUnlockStrategies(videoId, reason);
let unlockedPlayerResponse = {};
- // Try every strategy until one of them works
- unlockStrategies.every((strategy, index) => {
+ for (const strategy of unlockStrategies) {
+ if (strategy.skip) continue;
+
// Skip strategy if authentication is required and the user is not logged in
- if (strategy.skip || (strategy.requiresAuth && !isUserLoggedIn())) return true;
+ if (strategy.requiresAuth && !isUserLoggedIn()) continue;
- logger.info(`Trying Player Unlock Method #${index + 1} (${strategy.name})`);
+ logger.info(`Trying Player Unlock Method ${strategy.name}`);
try {
unlockedPlayerResponse = strategy.endpoint.getPlayer(strategy.payload, strategy.requiresAuth || strategy.optionalAuth);
} catch (err) {
- logger.error(err, `Player Unlock Method ${index + 1} failed with exception`);
+ logger.error(`Player unlock Method "${strategy.name}" failed with exception:`, err);
}
- const isStatusValid = Config.VALID_PLAYABILITY_STATUSES.includes(unlockedPlayerResponse?.playabilityStatus?.status);
+ const isStatusValid = VALID_PLAYABILITY_STATUSES.includes(unlockedPlayerResponse?.playabilityStatus?.status);
if (isStatusValid) {
/**
@@ -115,25 +105,146 @@ function getUnlockedPlayerResponse(videoId, reason) {
};
}
- /**
- * Workaround: Account proxy response currently does not include `playerConfig`
- *
- * Stays here until we rewrite the account proxy to only include the necessary and bare minimum response
- */
- if (strategy.payload.startTimeSecs && strategy.name === 'Account Proxy') {
- unlockedPlayerResponse.playerConfig = {
- playbackStartConfig: {
- startSeconds: strategy.payload.startTimeSecs,
- },
- };
- }
+ // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
+ cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };
+
+ return unlockedPlayerResponse;
}
+ }
+}
+
+export function isPlayerObject(parsedData) {
+ return (parsedData?.videoDetails && parsedData?.playabilityStatus) || typeof parsedData?.previewPlayabilityStatus === 'object';
+}
- return !isStatusValid;
- });
+export function isAgeRestricted(ytData) {
+ const playabilityStatus = ytData.previewPlayabilityStatus ?? ytData.playabilityStatus;
- // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
- cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };
+ if (!playabilityStatus?.status) return false;
+ if (playabilityStatus.desktopLegacyAgeGateReason) return true;
+ if (UNLOCKABLE_PLAYABILITY_STATUSES.includes(playabilityStatus.status)) return true;
+
+ // Fix to detect age restrictions on embed player
+ // see https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/85#issuecomment-946853553
+ return (
+ isEmbed
+ && playabilityStatus.errorScreen?.playerErrorMessageRenderer?.reason?.runs?.find((x) => x.navigationEndpoint)?.navigationEndpoint?.urlEndpoint?.url?.includes('/2802167')
+ );
+}
- return unlockedPlayerResponse;
+function getCurrentVideoStartTime(currentVideoId) {
+ // Check if the URL corresponds to the requested video
+ // This is not the case when the player gets preloaded for the next video in a playlist.
+ if (window.location.href.includes(currentVideoId)) {
+ // "t"-param on youtu.be urls
+ // "start"-param on embed player
+ // "time_continue" when clicking "watch on youtube" on embedded player
+ const urlParams = new URLSearchParams(window.location.search);
+ const startTimeString = (urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue'))?.replace('s', '');
+
+ if (startTimeString && !isNaN(startTimeString)) {
+ return parseInt(startTimeString);
+ }
+ }
+
+ return 0;
+}
+
+function getUnlockStrategies(videoId, reason) {
+ const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
+ const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
+ const signatureTimestamp = getSignatureTimestamp();
+ const startTimeSecs = getCurrentVideoStartTime(videoId);
+ const hl = getYtcfgValue('HL');
+
+ return [
+ /**
+ * Retrieve the video info by just adding `racyCheckOk` and `contentCheckOk` params
+ * This strategy can be used to bypass content warnings
+ */
+ {
+ name: 'Content Warning Bypass',
+ skip: !reason || !reason.includes('CHECK_REQUIRED'),
+ optionalAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName: clientName,
+ clientVersion: clientVersion,
+ hl,
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the video info by using the TVHTML5 Embedded client
+ * This client has no age restrictions in place (2022-03-28)
+ * See https://github.com/zerodytrash/YouTube-Internal-Clients
+ */
+ {
+ name: 'TV Embedded Player',
+ requiresAuth: false,
+ payload: {
+ context: {
+ client: {
+ clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
+ clientVersion: '2.0',
+ clientScreen: 'WATCH',
+ hl,
+ },
+ thirdParty: {
+ embedUrl: 'https://www.youtube.com/',
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ /**
+ * Retrieve the video info by using the WEB_CREATOR client in combination with user authentication
+ * Requires that the user is logged in. Can bypass the tightened age verification in the EU.
+ * See https://github.com/yt-dlp/yt-dlp/pull/600
+ */
+ {
+ name: 'Creator + Auth',
+ requiresAuth: true,
+ payload: {
+ context: {
+ client: {
+ clientName: 'WEB_CREATOR',
+ clientVersion: '1.20210909.07.00',
+ hl,
+ },
+ },
+ playbackContext: {
+ contentPlaybackContext: {
+ signatureTimestamp,
+ },
+ },
+ videoId,
+ startTimeSecs,
+ racyCheckOk: true,
+ contentCheckOk: true,
+ },
+ endpoint: innertube,
+ },
+ ];
}
diff --git a/src/config.js b/src/config.js
index 08c53dc..b21f0f5 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,51 +1,17 @@
-// Script configuration variables
-const UNLOCKABLE_PLAYABILITY_STATUSES = ['AGE_VERIFICATION_REQUIRED', 'AGE_CHECK_REQUIRED', 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
-const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];
-
-// These are the proxy servers that are sometimes required to unlock videos with age restrictions.
-// You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
-// To learn what information is transferred, please read: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#privacy
-const ACCOUNT_PROXY_SERVER_HOST = 'https://youtube-proxy.zerody.one';
-const VIDEO_PROXY_SERVER_HOST = 'https://ny.4everproxy.com';
-
// User needs to confirm the unlock process on embedded player?
-let ENABLE_UNLOCK_CONFIRMATION_EMBED = true;
+export let ENABLE_UNLOCK_CONFIRMATION_EMBED = true;
// Show notification?
-let ENABLE_UNLOCK_NOTIFICATION = true;
+export let ENABLE_UNLOCK_NOTIFICATION = true;
// Disable content warnings?
-let SKIP_CONTENT_WARNINGS = true;
-
-// Some Innertube bypass methods require the following authentication headers of the currently logged in user.
-const GOOGLE_AUTH_HEADER_NAMES = ['Authorization', 'X-Goog-AuthUser', 'X-Origin'];
-
-/**
- * The SQP parameter length is different for blurred thumbnails.
- * They contain much less information, than normal thumbnails.
- * The thumbnail SQPs tend to have a long and a short version.
- */
-const BLURRED_THUMBNAIL_SQP_LENGTHS = [
- 32, // Mobile (SHORT)
- 48, // Desktop Playlist (SHORT)
- 56, // Desktop (SHORT)
- 68, // Mobile (LONG)
- 72, // Mobile Shorts
- 84, // Desktop Playlist (LONG)
- 88, // Desktop (LONG)
-];
+export let SKIP_CONTENT_WARNINGS = true;
-// small hack to prevent tree shaking on these exports
+// WORKAROUND: Do not treeshake
export default window[Symbol()] = {
- UNLOCKABLE_PLAYABILITY_STATUSES,
- VALID_PLAYABILITY_STATUSES,
- ACCOUNT_PROXY_SERVER_HOST,
- VIDEO_PROXY_SERVER_HOST,
ENABLE_UNLOCK_CONFIRMATION_EMBED,
ENABLE_UNLOCK_NOTIFICATION,
SKIP_CONTENT_WARNINGS,
- GOOGLE_AUTH_HEADER_NAMES,
- BLURRED_THUMBNAIL_SQP_LENGTHS,
};
if (__BUILD_TARGET__ === 'WEB_EXTENSION') {
diff --git a/src/utils/logger.js b/src/logger.js
similarity index 60%
rename from src/utils/logger.js
rename to src/logger.js
index 868a0dc..d562f7d 100644
--- a/src/utils/logger.js
+++ b/src/logger.js
@@ -1,11 +1,9 @@
-import { getYtcfgValue } from '../utils';
-
const logPrefix = '%cSimple-YouTube-Age-Restriction-Bypass:';
const logPrefixStyle = 'background-color: #1e5c85; color: #fff; font-size: 1.2em;';
const logSuffix = '\uD83D\uDC1E You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues';
export function error(err, msg) {
- console.error(logPrefix, logPrefixStyle, msg, err, getYtcfgDebugString(), '\n\n', logSuffix);
+ console.error(logPrefix, logPrefixStyle, msg, err, '\n\n', logSuffix);
if (window.SYARB_CONFIG) {
window.dispatchEvent(
new CustomEvent('SYARB_LOG_ERROR', {
@@ -30,17 +28,3 @@ export function info(msg) {
);
}
}
-
-export function getYtcfgDebugString() {
- try {
- return (
- `InnertubeConfig: `
- + `innertubeApiKey: ${getYtcfgValue('INNERTUBE_API_KEY')} `
- + `innertubeClientName: ${getYtcfgValue('INNERTUBE_CLIENT_NAME')} `
- + `innertubeClientVersion: ${getYtcfgValue('INNERTUBE_CLIENT_VERSION')} `
- + `loggedIn: ${getYtcfgValue('LOGGED_IN')} `
- );
- } catch (err) {
- return `Failed to access config: ${err}`;
- }
-}
diff --git a/src/main.js b/src/main.js
index 187b962..91fec64 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,42 +1,91 @@
-import './config';
-import * as inspectors from './components/inspectors';
-import * as interceptors from './components/interceptors';
-import * as requestPreprocessor from './components/requestPreprocessor';
-import * as thumbnailFix from './components/thumbnailFix';
-import * as unlocker from './components/unlocker';
-import * as logger from './utils/logger';
-
-try {
- interceptors.attachInitialDataInterceptor(processYtData);
- interceptors.attachJsonInterceptor(processYtData);
- interceptors.attachXhrOpenInterceptor(requestPreprocessor.handleXhrOpen);
- interceptors.attachRequestInterceptor(requestPreprocessor.handleFetchRequest);
-} catch (err) {
- logger.error(err, 'Error while attaching data interceptors');
-}
+// Leave config on top
+import './config.js';
+import * as thumbnailFix from './components/thumbnailFix.js';
+import * as nextUnlocker from './components/unlocker/next.js';
+import * as playerUnlocker from './components/unlocker/player.js';
+import * as config from './config.js';
+import * as logger from './logger.js';
+import { createDeepCopy, GOOGLE_AUTH_HEADER_NAMES } from './utils.js';
+
+/**
+ * And here we deal with YouTube's crappy initial data (present in page source) and the problems that occur when intercepting that data.
+ * YouTube has some protections in place that make it difficult to intercept and modify the global ytInitialPlayerResponse variable.
+ * The easiest way would be to set a descriptor on that variable to change the value directly on declaration.
+ * But some adblockers define their own descriptors on the ytInitialPlayerResponse variable, which makes it hard to register another descriptor on it.
+ * As a workaround only the relevant playerResponse property of the ytInitialPlayerResponse variable will be intercepted.
+ * This is achieved by defining a descriptor on the object prototype for that property, which affects any object with a `playerResponse` property.
+ */
+interceptObjectProperty('playerResponse', (obj, playerResponse) => {
+ logger.info(`playerResponse property set, contains sidebar: ${!!obj.response}`);
+
+ // The same object also contains the sidebar data and video description
+ if (obj.response) processYtData(obj.response);
+
+ // If the script is executed too late and the bootstrap data has already been processed,
+ // a reload of the player can be forced by creating a deep copy of the object.
+ // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
+ return processYtData(playerResponse) ? createDeepCopy(playerResponse) : playerResponse;
+});
+
+// The global `ytInitialData` variable can be modified on the fly.
+// It contains search results, sidebar data and meta information
+// Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
+window.addEventListener('DOMContentLoaded', () => {
+ if (window.ytInitialData) {
+ processYtData(window.ytInitialData);
+ }
+});
+
+JSON.parse = new Proxy(JSON.parse, {
+ construct(target, args) {
+ const data = Reflect.construct(target, args);
+ processYtData(data);
+ return data;
+ },
+});
+
+XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
+ construct(target, args) {
+ const [method, url] = args;
+ try {
+ if (typeof url === 'string' && url.indexOf('https://') !== -1) {
+ handleXhrOpen(method, url, this);
+ }
+ } catch (err) {
+ logger.error(err, `Failed to intercept XMLHttpRequest.open()`);
+ }
+
+ return Reflect.construct(target, args);
+ },
+});
+
+Request = new Proxy(Request, {
+ construct(target, args) {
+ const [url, options] = args;
+ try {
+ if (typeof url === 'string' && url.indexOf('https://') !== -1) {
+ handleFetchRequest(url, options);
+ }
+ } catch (err) {
+ logger.error(err, `Failed to intercept Request()`);
+ }
+
+ return Reflect.construct(target, args);
+ },
+});
function processYtData(ytData) {
try {
- // Player Unlock #1: Initial page data structure and response from `/youtubei/v1/player` XHR request
- if (inspectors.player.isPlayerObject(ytData) && inspectors.player.isAgeRestricted(ytData.playabilityStatus)) {
- unlocker.unlockPlayerResponse(ytData);
- } // Player Unlock #2: Embedded Player inital data structure
- else if (inspectors.player.isEmbeddedPlayerObject(ytData) && inspectors.player.isAgeRestricted(ytData.previewPlayabilityStatus)) {
- unlocker.unlockPlayerResponse(ytData);
+ if (playerUnlocker.isPlayerObject(ytData) && playerUnlocker.isAgeRestricted(ytData)) {
+ return playerUnlocker.unlockResponse(ytData);
}
} catch (err) {
logger.error(err, 'Video unlock failed');
}
try {
- // Unlock sidebar watch next feed (sidebar) and video description
- if (inspectors.next.isWatchNextObject(ytData) && inspectors.next.isWatchNextSidebarEmpty(ytData)) {
- unlocker.unlockNextResponse(ytData);
- }
-
- // Mobile version
- if (inspectors.next.isWatchNextObject(ytData.response) && inspectors.next.isWatchNextSidebarEmpty(ytData.response)) {
- unlocker.unlockNextResponse(ytData.response);
+ if (nextUnlocker.isWatchNextObject(ytData) && nextUnlocker.isWatchNextSidebarEmpty(ytData)) {
+ nextUnlocker.unlockResponse(ytData);
}
} catch (err) {
logger.error(err, 'Sidebar unlock failed');
@@ -44,12 +93,126 @@ function processYtData(ytData) {
try {
// Unlock blurry video thumbnails in search results
- if (inspectors.search.isSearchResult(ytData)) {
+ if (isSearchResult(ytData)) {
thumbnailFix.processThumbnails(ytData);
}
} catch (err) {
logger.error(err, 'Thumbnail unlock failed');
}
+}
+
+function interceptObjectProperty(prop, onSet) {
+ // Allow other userscripts to decorate this descriptor, if they do something similar
+ const dataKey = '__SYARB_' + prop;
+ const { get: getter, set: setter } = Object.getOwnPropertyDescriptor(Object.prototype, prop) ?? {
+ set(value) {
+ this[dataKey] = value;
+ },
+ get() {
+ return this[dataKey];
+ },
+ };
+
+ // Intercept the given property on any object
+ // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
+ Object.defineProperty(Object.prototype, prop, {
+ set(value) {
+ setter.call(this, value ? onSet(this, value) : value);
+ },
+ get() {
+ return getter.call(this);
+ },
+ configurable: true,
+ });
+}
+
+function isSearchResult(parsedData) {
+ return (
+ typeof parsedData?.contents?.twoColumnSearchResultsRenderer === 'object' // Desktop initial results
+ || parsedData?.contents?.sectionListRenderer?.targetId === 'search-feed' // Mobile initial results
+ || parsedData?.onResponseReceivedCommands?.find((x) => x.appendContinuationItemsAction)?.appendContinuationItemsAction?.targetId === 'search-feed' // Desktop & Mobile scroll continuation
+ );
+}
- return ytData;
+function attachGenericInterceptor(obj, prop, onCall) {
+ if (!obj || typeof obj[prop] !== 'function') {
+ return;
+ }
+
+ const original = obj[prop];
+
+ obj[prop] = function(...args) {
+ try {
+ onCall(args);
+ } catch {}
+ original.apply(this, args);
+ };
+}
+
+/**
+ * Handles XMLHttpRequests and
+ * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
+ * - Store auth headers for the authentication of further unlock requests.
+ * - Add "content check ok" flags to request bodys
+ */
+function handleXhrOpen(method, url, xhr) {
+ const url_obj = new URL(url);
+
+ if (url_obj.pathname.startsWith('/youtubei/')) {
+ // Store auth headers in storage for further usage.
+ attachGenericInterceptor(xhr, 'setRequestHeader', ([key, value]) => {
+ if (GOOGLE_AUTH_HEADER_NAMES.includes(key)) {
+ localStorage.setItem('SYARB_' + key, JSON.stringify(value));
+ }
+ });
+ }
+
+ if (config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
+ // Add content check flags to player and next request (this will skip content warnings)
+ attachGenericInterceptor(xhr, 'send', (args) => {
+ if (typeof args[0] === 'string') {
+ args[0] = setContentCheckOk(args[0]);
+ }
+ });
+ }
+}
+
+/**
+ * Handles Fetch requests and
+ * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
+ * - Store auth headers for the authentication of further unlock requests.
+ * - Add "content check ok" flags to request bodys
+ */
+function handleFetchRequest(url, requestOptions) {
+ const url_obj = new URL(url);
+
+ if (url_obj.pathname.startsWith('/youtubei/') && requestOptions.headers) {
+ // Store auth headers in authStorage for further usage.
+ for (const key in requestOptions.headers) {
+ if (GOOGLE_AUTH_HEADER_NAMES.includes(key)) {
+ localStorage.setItem('SYARB_' + key, JSON.stringify(requestOptions.headers[key]));
+ }
+ }
+ }
+
+ if (config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
+ // Add content check flags to player and next request (this will skip content warnings)
+ requestOptions.body = setContentCheckOk(requestOptions.body);
+ }
+}
+
+/**
+ * Adds `contentCheckOk` and `racyCheckOk` to the given json data (if the data contains a video id)
+ * @returns {string} The modified json
+ */
+function setContentCheckOk(bodyJson) {
+ try {
+ const parsedBody = JSON.parse(bodyJson);
+ if (parsedBody.videoId) {
+ parsedBody.contentCheckOk = true;
+ parsedBody.racyCheckOk = true;
+ return JSON.stringify(parsedBody);
+ }
+ } catch {}
+ return bodyJson;
}
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..d56697d
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,94 @@
+export const nativeJSONParse = JSON.parse;
+export const nativeXMLHttpRequestOpen = XMLHttpRequest.prototype.open;
+
+export const isDesktop = location.host !== 'm.youtube.com';
+export const isMusic = location.host === 'music.youtube.com';
+export const isEmbed = location.pathname.indexOf('/embed/') === 0;
+export const isConfirmed = location.search.includes('unlock_confirmed');
+
+// Some Innertube bypass methods require the following authentication headers of the currently logged in user.
+export const GOOGLE_AUTH_HEADER_NAMES = ['Authorization', 'X-Goog-AuthUser', 'X-Origin'];
+
+// WORKAROUND: TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
+if (window.trustedTypes) {
+ if (!window.trustedTypes.defaultPolicy) {
+ const passThroughFn = (x) => x;
+ window.trustedTypes.createPolicy('default', {
+ createHTML: passThroughFn,
+ createScriptURL: passThroughFn,
+ createScript: passThroughFn,
+ });
+ }
+}
+
+export function createElement(tagName, options) {
+ const node = document.createElement(tagName);
+ options && Object.assign(node, options);
+ return node;
+}
+
+export function pageLoaded() {
+ if (document.readyState === 'complete') return Promise.resolve();
+
+ const { promise, resolve } = Promise.withResolvers();
+
+ window.addEventListener('load', resolve, { once: true });
+
+ return promise;
+}
+
+export function createDeepCopy(obj) {
+ return nativeJSONParse(JSON.stringify(obj));
+}
+
+export function getYtcfgValue(name) {
+ return window.ytcfg?.get(name);
+}
+
+export function getSignatureTimestamp() {
+ return (
+ getYtcfgValue('STS')
+ || (() => {
+ // STS is missing on embedded player. Retrieve from player base script as fallback...
+ const playerBaseJsPath = document.querySelector('script[src*="/base.js"]')?.src;
+
+ if (!playerBaseJsPath) return;
+
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('GET', playerBaseJsPath, false);
+ xmlhttp.send(null);
+
+ return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
+ })()
+ );
+}
+
+export function isUserLoggedIn() {
+ // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
+ if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
+ if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
+ if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;
+
+ return false;
+}
+
+export function waitForElement(elementSelector, timeout) {
+ const { promise, resolve, reject } = Promise.withResolvers();
+
+ const checkDomInterval = setInterval(() => {
+ const elem = document.querySelector(elementSelector);
+ if (elem) {
+ clearInterval(checkDomInterval);
+ resolve(elem);
+ }
+ }, 100);
+
+ if (timeout) {
+ setTimeout(() => {
+ clearInterval(checkDomInterval);
+ reject();
+ }, timeout);
+ }
+
+ return promise;
+}
diff --git a/src/utils/index.js b/src/utils/index.js
deleted file mode 100644
index d3eac45..0000000
--- a/src/utils/index.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { nativeJSONParse } from '../components/interceptors/natives';
-
-export const isDesktop = window.location.host !== 'm.youtube.com';
-export const isMusic = window.location.host === 'music.youtube.com';
-export const isEmbed = window.location.pathname.indexOf('/embed/') === 0;
-export const isConfirmed = window.location.search.includes('unlock_confirmed');
-
-export class Deferred {
- constructor() {
- return Object.assign(
- new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- }),
- this,
- );
- }
-}
-
-// WORKAROUND: TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
-if (window.trustedTypes && trustedTypes.createPolicy) {
- if (!trustedTypes.defaultPolicy) {
- const passThroughFn = (x) => x;
- trustedTypes.createPolicy('default', {
- createHTML: passThroughFn,
- createScriptURL: passThroughFn,
- createScript: passThroughFn,
- });
- }
-}
-
-export function createElement(tagName, options) {
- const node = document.createElement(tagName);
- options && Object.assign(node, options);
- return node;
-}
-
-export function isObject(obj) {
- return obj !== null && typeof obj === 'object';
-}
-
-export function findNestedObjectsByAttributeNames(object, attributeNames) {
- var results = [];
-
- // Does the current object match the attribute conditions?
- if (attributeNames.every((key) => typeof object[key] !== 'undefined')) {
- results.push(object);
- }
-
- // Diggin' deeper for each nested object (recursive)
- Object.keys(object).forEach((key) => {
- if (object[key] && typeof object[key] === 'object') {
- results.push(...findNestedObjectsByAttributeNames(object[key], attributeNames));
- }
- });
-
- return results;
-}
-
-export function getCookie(name) {
- const value = `; ${document.cookie}`;
- const parts = value.split(`; ${name}=`);
- if (parts.length === 2) return parts.pop().split(';').shift();
-}
-
-export function pageLoaded() {
- if (document.readyState === 'complete') return Promise.resolve();
-
- const deferred = new Deferred();
-
- window.addEventListener('load', deferred.resolve, { once: true });
-
- return deferred;
-}
-
-export function createDeepCopy(obj) {
- return nativeJSONParse(JSON.stringify(obj));
-}
-
-export function getYtcfgValue(name) {
- return window.ytcfg?.get(name);
-}
-
-export function getSignatureTimestamp() {
- return (
- getYtcfgValue('STS')
- || (() => {
- // STS is missing on embedded player. Retrieve from player base script as fallback...
- const playerBaseJsPath = document.querySelector('script[src*="/base.js"]')?.src;
-
- if (!playerBaseJsPath) return;
-
- const xmlhttp = new XMLHttpRequest();
- xmlhttp.open('GET', playerBaseJsPath, false);
- xmlhttp.send(null);
-
- return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
- })()
- );
-}
-
-export function isUserLoggedIn() {
- // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
- if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
- if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
- if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;
-
- return false;
-}
-
-export function getCurrentVideoStartTime(currentVideoId) {
- // Check if the URL corresponds to the requested video
- // This is not the case when the player gets preloaded for the next video in a playlist.
- if (window.location.href.includes(currentVideoId)) {
- // "t"-param on youtu.be urls
- // "start"-param on embed player
- // "time_continue" when clicking "watch on youtube" on embedded player
- const urlParams = new URLSearchParams(window.location.search);
- const startTimeString = (urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue'))?.replace('s', '');
-
- if (startTimeString && !isNaN(startTimeString)) {
- return parseInt(startTimeString);
- }
- }
-
- return 0;
-}
-
-export function setUrlParams(params) {
- const urlParams = new URLSearchParams(window.location.search);
- for (const paramName in params) {
- urlParams.set(paramName, params[paramName]);
- }
- window.location.search = urlParams;
-}
-
-export function waitForElement(elementSelector, timeout) {
- const deferred = new Deferred();
-
- const checkDomInterval = setInterval(() => {
- const elem = document.querySelector(elementSelector);
- if (elem) {
- clearInterval(checkDomInterval);
- deferred.resolve(elem);
- }
- }, 100);
-
- if (timeout) {
- setTimeout(() => {
- clearInterval(checkDomInterval);
- deferred.reject();
- }, timeout);
- }
-
- return deferred;
-}
diff --git a/userscript.config.js b/userscript.config.js
deleted file mode 100644
index aaa925c..0000000
--- a/userscript.config.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// ==UserScript==
-// @name Simple YouTube Age Restriction Bypass
-// @description Watch age restricted videos on YouTube without login and without age verification 😎
-// @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen 😎
-// @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge 😎
-// @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età 😎
-// @icon https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/v2.5.4/src/extension/icon/icon_64.png
-// @version __BUILD_VERSION__
-// @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
-// @updateURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/main/dist/Simple-YouTube-Age-Restriction-Bypass.user.js
-// @license MIT
-// @match https://www.youtube.com/*
-// @match https://www.youtube-nocookie.com/*
-// @match https://m.youtube.com/*
-// @match https://music.youtube.com/*
-// @grant none
-// @run-at document-start
-// @compatible chrome
-// @compatible firefox
-// @compatible opera
-// @compatible edge
-// @compatible safari
-// ==/UserScript==
-
-/*
- This is a transpiled version to achieve a clean code base and better browser compatibility.
- You can find the nicely readable source code at https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
-*/