1+ const FIRST_PROXY = 'https://twitfix.duckdns.org/fetch?url='
2+ const SECOND_PROXY = 'https://twitfix.chimildic.workers.dev/fetch?url='
3+
4+ let is1080Enabled = localStorage . getItem ( 'twitfix_is1080Enabled' )
5+ if ( is1080Enabled === 'true' || is1080Enabled === null ) {
6+ hookFetchFunction ( )
7+ hookWorkerClass ( )
8+ console . log ( "[Twitfix] Включено 🟢" )
9+ } else {
10+ console . log ( "[Twitfix] Выключено 🔴" )
11+ }
12+
13+ function hookFetchFunction ( ) {
14+ const originalFetch = window . fetch ;
15+
16+ window . fetch = async function ( url , options = { } ) {
17+ let isRequestFound = options . method === 'POST' &&
18+ url . startsWith ( 'https://gql.twitch.tv/gql' ) &&
19+ typeof options . body === 'string' &&
20+ options . body . includes ( 'PlaybackAccessToken' ) &&
21+ localStorage . getItem ( 'twitfix_is1440Enabled' ) === 'true'
22+
23+ if ( isRequestFound ) {
24+ if ( isQualityEnabled ( options . body ) ) {
25+ return fetchToken ( url , options )
26+ } else {
27+ console . log ( '[Twitfix] Запрос найден, но 1440p отключен' )
28+ return originalFetch ( url , options ) ;
29+ }
30+ } else {
31+ return originalFetch ( url , options ) ;
32+ }
33+ } ;
34+
35+ function isQualityEnabled ( body ) {
36+ try {
37+ if ( location . pathname . startsWith ( '/videos' ) ) {
38+ return true
39+ }
40+
41+ let targetChannels = localStorage . getItem ( 'twitfix_channels' )
42+ let json = JSON . parse ( body )
43+ if ( Array . isArray ( json ) ) {
44+ for ( let item of json ) {
45+ if ( item . operationName . includes ( 'PlaybackAccessToken' ) && targetChannels . includes ( item . variables . login ) ) {
46+ return true
47+ }
48+ }
49+ } else {
50+ return json . operationName . includes ( 'PlaybackAccessToken' ) && targetChannels . includes ( json . variables . login )
51+ }
52+ } catch ( error ) {
53+ console . error ( '[Twitfix] Ошибка в isQualityEnabled' , error )
54+ }
55+ return false
56+ }
57+
58+ async function fetchToken ( url , options ) {
59+ try {
60+ let proxyUrl = FIRST_PROXY + encodeURIComponent ( url ) ;
61+ let proxyOptions = { ...options , signal : AbortSignal . timeout ( 5000 ) } ;
62+ let proxyResponse = await originalFetch ( proxyUrl , proxyOptions ) ;
63+ if ( proxyResponse . ok ) {
64+ return proxyResponse ;
65+ } else {
66+ console . warn ( '[Twitfix] Прокси ответил ошибкой:' , proxyResponse . status ) ;
67+ return originalFetch ( url , options ) ;
68+ }
69+ } catch ( error ) {
70+ console . warn ( '[Twitfix] Прокси недоступен:' , error ) ;
71+ return originalFetch ( url , options ) ;
72+ }
73+ }
74+ }
75+
76+ function hookWorkerClass ( ) {
77+ const OriginalWorker = window . Worker ;
78+
79+ window . Worker = function ( url , options ) {
80+ let loaderURL = url
81+ if ( url . startsWith ( 'blob:' ) ) {
82+ let loaderBlob = new Blob ( [ createLoader ( url ) ] , { type : 'application/javascript' } ) ;
83+ loaderURL = URL . createObjectURL ( loaderBlob ) ;
84+ }
85+ return new OriginalWorker ( loaderURL , options ) ;
86+ }
87+ }
88+
89+ function createLoader ( url ) {
90+ return `
91+ const PROXY_ENDPOINTS = [
92+ '${ FIRST_PROXY } ',
93+ '${ SECOND_PROXY } ',
94+ ];
95+ const PROXY_TIMEOUT_MS = 5000;
96+ const originalFetch = self.fetch;
97+
98+ self.fetch = async function (url, options) {
99+ if (!url.startsWith('https://usher.ttvnw.net')) {
100+ return originalFetch(url, options);
101+ }
102+
103+ for (const base of PROXY_ENDPOINTS) {
104+ try {
105+ const proxyUrl = base + encodeURIComponent(url);
106+ const proxyOptions = { ...options, signal: AbortSignal.timeout(5000) };
107+ const proxyResponse = await originalFetch(proxyUrl, proxyOptions);
108+ if (proxyResponse.ok) {
109+ if (proxyResponse.status == 200) {
110+ console.log('[Twitfix] Шалость удалась 🪄')
111+ }
112+ return proxyResponse;
113+ } else {
114+ console.warn('[Twitfix] Прокси', base, 'ответил ошибкой:', proxyResponse.status);
115+ }
116+ } catch (error) {
117+ console.warn('[Twitfix] Прокси', base, 'недоступен:', error);
118+ }
119+ }
120+
121+ return originalFetch(url, options);
122+ };
123+
124+ const messageQueue = [];
125+ const realMessageListeners = [];
126+ let realOnMessage = null;
127+
128+ const originalAddEventListener = self.addEventListener;
129+ self.addEventListener = function (type, listener, options) {
130+ if (type === 'message') {
131+ realMessageListeners.push(listener);
132+ } else {
133+ originalAddEventListener.call(self, type, listener, options);
134+ }
135+ };
136+
137+ self.onmessage = (event) => {
138+ if (realMessageListeners.length > 0) {
139+ for (const listener of realMessageListeners) {
140+ listener.call(self, event);
141+ }
142+ } else if (realOnMessage) {
143+ realOnMessage(event);
144+ } else {
145+ messageQueue.push(event);
146+ }
147+ };
148+
149+ fetch('${ url } ')
150+ .then(r => r.text())
151+ .then(code => {
152+ const wrappedCode = \`
153+ console.log('[Twitfix] Воркер запущен');
154+ \${code}
155+ \`;
156+
157+ let blob = new Blob([wrappedCode], { type: 'application/javascript' });
158+ let blobURL = URL.createObjectURL(blob);
159+ importScripts(blobURL);
160+
161+ if (typeof self.onmessage === 'function') {
162+ realOnMessage = self.onmessage;
163+ }
164+
165+ if (realOnMessage || realMessageListeners.length > 0) {
166+ for (const event of messageQueue) {
167+ self.onmessage(event);
168+ }
169+ messageQueue.length = 0;
170+ } else {
171+ console.warn('[Twitfix] Не найден обработчик сообщений после importScripts');
172+ }
173+ })
174+ .catch(err => {
175+ console.error('[Twitfix] Ошибка загрузки воркера:', err);
176+ });
177+ ` ;
178+ }
0 commit comments