Skip to content

Commit ca65618

Browse files
committed
Error detection
1 parent 4f8c118 commit ca65618

File tree

7 files changed

+157
-4
lines changed

7 files changed

+157
-4
lines changed

injected/src/features/duckplayer-native/duckplayer-native.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { getCurrentTimestamp } from './getCurrentTimestamp.js';
2-
import { mediaControl } from './mediaControl.js';
3-
import { muteAudio } from './muteAudio.js';
4-
import { serpNotify } from './serpNotify.js';
1+
import { getCurrentTimestamp } from './get-current-timestamp.js';
2+
import { mediaControl } from './media-control.js';
3+
import { muteAudio } from './mute-audio.js';
4+
import { serpNotify } from './serp-notify.js';
5+
import { ErrorDetection } from './error-detection.js';
56

67
/**
78
*
@@ -11,6 +12,9 @@ import { serpNotify } from './serpNotify.js';
1112
export async function initDuckPlayerNative(messages) {
1213
/** @type {import("../duck-player-native.js").InitialSettings} */
1314
let initialSetup;
15+
/** @type {(() => void|null)[]} */
16+
const sideEffects = [];
17+
1418
try {
1519
initialSetup = await messages.initialSetup();
1620
} catch (e) {
@@ -42,4 +46,13 @@ export async function initDuckPlayerNative(messages) {
4246
console.log('SERP PROXY');
4347
serpNotify();
4448
});
49+
50+
/* Start error detection */
51+
const errorDetection = new ErrorDetection(messages);
52+
const destroy = errorDetection.observe();
53+
if (destroy) sideEffects.push(destroy);
54+
55+
return async () => {
56+
return await Promise.all(sideEffects.map((destroy) => destroy()));
57+
};
4558
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/** @typedef {"age-restricted" | "sign-in-required" | "no-embed" | "unknown"} YouTubeError */
2+
3+
/** @type {Record<string,YouTubeError>} */
4+
export const YOUTUBE_ERRORS = {
5+
ageRestricted: 'age-restricted',
6+
signInRequired: 'sign-in-required',
7+
noEmbed: 'no-embed',
8+
unknown: 'unknown',
9+
};
10+
11+
/**
12+
* Detects YouTube errors based on DOM queries
13+
*/
14+
export class ErrorDetection {
15+
/** @type {import('./native-messages.js').DuckPlayerNativeMessages} */
16+
messages;
17+
18+
constructor(messages) {
19+
this.messages = messages;
20+
this.settings = {
21+
// TODO: Get settings from native
22+
signInRequiredSelector: '[href*="//support.google.com/youtube/answer/3037019"]',
23+
};
24+
this.observe();
25+
}
26+
27+
observe() {
28+
console.log('Setting up error detection...');
29+
const documentBody = document?.body;
30+
if (documentBody) {
31+
// Check if iframe already contains error
32+
if (this.checkForError(documentBody)) {
33+
const error = this.getErrorType();
34+
this.messages.onYoutubeError(error);
35+
return null;
36+
}
37+
38+
// Create a MutationObserver instance
39+
const observer = new MutationObserver(this.handleMutation.bind(this));
40+
41+
// Start observing the iframe's document for changes
42+
observer.observe(documentBody, {
43+
childList: true,
44+
subtree: true, // Observe all descendants of the body
45+
});
46+
47+
return () => {
48+
observer.disconnect();
49+
};
50+
}
51+
52+
return null;
53+
}
54+
55+
/**
56+
* Mutation handler that checks new nodes for error states
57+
*
58+
* @type {MutationCallback}
59+
*/
60+
handleMutation(mutationsList) {
61+
for (const mutation of mutationsList) {
62+
if (mutation.type === 'childList') {
63+
mutation.addedNodes.forEach((node) => {
64+
if (this.checkForError(node)) {
65+
console.log('A node with an error has been added to the document:', node);
66+
const error = this.getErrorType();
67+
this.messages.onYoutubeError(error);
68+
}
69+
});
70+
}
71+
}
72+
}
73+
74+
/**
75+
* Attempts to detect the type of error in the YouTube embed iframe
76+
* @returns {YouTubeError}
77+
*/
78+
getErrorType() {
79+
const currentWindow = /** @type {Window & typeof globalThis & { ytcfg: object }} */ (window);
80+
let playerResponse;
81+
82+
try {
83+
playerResponse = JSON.parse(currentWindow.ytcfg?.get('PLAYER_VARS')?.embedded_player_response);
84+
} catch (e) {
85+
console.log('Could not parse player response', e);
86+
}
87+
88+
if (typeof playerResponse === 'object') {
89+
const {
90+
previewPlayabilityStatus: { desktopLegacyAgeGateReason, status },
91+
} = playerResponse;
92+
93+
// 1. Check for UNPLAYABLE status
94+
if (status === 'UNPLAYABLE') {
95+
// 1.1. Check for presence of desktopLegacyAgeGateReason
96+
if (desktopLegacyAgeGateReason === 1) {
97+
return YOUTUBE_ERRORS.ageRestricted;
98+
}
99+
100+
// 1.2. Fall back to embed not allowed error
101+
return YOUTUBE_ERRORS.noEmbed;
102+
}
103+
104+
// 2. Check for sign-in support link
105+
try {
106+
if (this.settings?.signInRequiredSelector && !!document.querySelector(this.settings.signInRequiredSelector)) {
107+
return YOUTUBE_ERRORS.signInRequired;
108+
}
109+
} catch (e) {
110+
console.log('Sign-in required query failed', e);
111+
}
112+
}
113+
114+
// 3. Fall back to unknown error
115+
return YOUTUBE_ERRORS.unknown;
116+
}
117+
118+
/**
119+
* Analyses a node and its children to determine if it contains an error state
120+
*
121+
* @param {Node} [node]
122+
*/
123+
checkForError(node) {
124+
if (node?.nodeType === Node.ELEMENT_NODE) {
125+
const element = /** @type {HTMLElement} */ (node);
126+
// Check if element has the error class or contains any children with that class
127+
return element.classList.contains('ytp-error') || !!element.querySelector('.ytp-error');
128+
}
129+
130+
return false;
131+
}
132+
}

injected/src/features/duckplayer-native/getCurrentTimestamp.js renamed to injected/src/features/duckplayer-native/get-current-timestamp.js

File renamed without changes.

injected/src/features/duckplayer-native/mediaControl.js renamed to injected/src/features/duckplayer-native/media-control.js

File renamed without changes.
File renamed without changes.

injected/src/features/duckplayer-native/native-messages.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,12 @@ export class DuckPlayerNativeMessages {
6868
onSerpNotify(callback) {
6969
return this.messaging.subscribe('onSerpNotify', callback);
7070
}
71+
72+
/**
73+
* Notifies browser of YouTube error
74+
* @param {string} error
75+
*/
76+
onYoutubeError(error) {
77+
this.messaging.notify('onYoutubeError', { error });
78+
}
7179
}

injected/src/features/duckplayer-native/serpNotify.js renamed to injected/src/features/duckplayer-native/serp-notify.js

File renamed without changes.

0 commit comments

Comments
 (0)