Skip to content
This repository was archived by the owner on Jul 23, 2025. It is now read-only.

Commit ab489ff

Browse files
committed
First commit
0 parents  commit ab489ff

File tree

10 files changed

+531
-0
lines changed

10 files changed

+531
-0
lines changed

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Twitfix
2+
3+
Доступ к качеству 1080p/1440p на Twitch
4+
5+
- [Установить для Chromium](https://chromewebstore.google.com/detail/ibfcagfjojdmcllobfnciccbgapngpic) (Chrome, Яндекс, Opera и т.д.)
6+
- [Установить для Firefox](https://addons.mozilla.org/firefox/addon/twitfix/)

src/background.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
chrome.runtime.onInstalled.addListener(onInstalled)
2+
3+
function onInstalled(details) {
4+
chrome.storage.local.get(undefined, (storage) => {
5+
chrome.storage.local.set({
6+
is1080Enabled: storage.hasOwnProperty('is1080Enabled') ? storage.is1080Enabled : true,
7+
is1440Enabled: storage.hasOwnProperty('is1440Enabled') ? storage.is1440Enabled : false,
8+
channels: storage.hasOwnProperty('channels') ? storage.channels : 'blackufa dangar evikey juice recrent ',
9+
})
10+
if (storage.hasOwnProperty('isTwitfixEnabled')) {
11+
chrome.storage.local.remove('isTwitfixEnabled')
12+
}
13+
})
14+
}

src/content.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
chrome.storage.local.get(undefined, onGetStorage)
2+
chrome.storage.onChanged.addListener(onStorageChanged)
3+
4+
function onGetStorage(storage) {
5+
Object.entries(storage).forEach(([key, value]) => {
6+
setProperty(key, value)
7+
})
8+
}
9+
10+
function onStorageChanged(changes) {
11+
Object.entries(changes).forEach(([key, changeItem]) => {
12+
setProperty(key, changeItem.newValue)
13+
key == 'is1080Enabled' && setTimeout(() => location.reload, 200)
14+
})
15+
}
16+
17+
function setProperty(key, value) {
18+
localStorage.setItem(`twitfix_${key}`, value)
19+
}

src/icons/icon128.png

1.57 KB
Loading

src/icons/icon256.png

10.3 KB
Loading

src/icons/slide-1.png

7.96 KB
Loading

src/injection.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
}

src/manifest.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "Twitfix",
3+
"description": "Доступ к качеству 1080p/1440p на Twitch",
4+
"version": "1.3.0",
5+
"manifest_version": 3,
6+
"author": "Chimildic",
7+
"host_permissions": [
8+
"https://*.twitch.tv/*"
9+
],
10+
"permissions": [
11+
"storage"
12+
],
13+
"content_scripts": [
14+
{
15+
"world": "MAIN",
16+
"run_at": "document_start",
17+
"all_frames": true,
18+
"matches": [
19+
"https://*.twitch.tv/*"
20+
],
21+
"js": [
22+
"injection.js"
23+
]
24+
},
25+
{
26+
"run_at": "document_start",
27+
"all_frames": true,
28+
"matches": [
29+
"https://*.twitch.tv/*"
30+
],
31+
"js": [
32+
"content.js"
33+
]
34+
}
35+
],
36+
"web_accessible_resources": [
37+
{
38+
"matches": [
39+
"https://*.twitch.tv/*"
40+
],
41+
"resources": [
42+
"injection.js"
43+
],
44+
"use_dynamic_url": false
45+
}
46+
],
47+
"background": {
48+
"service_worker": "background.js"
49+
},
50+
"action": {
51+
"default_popup": "popup.html"
52+
},
53+
"icons": {
54+
"256": "icons/icon256.png"
55+
}
56+
}

0 commit comments

Comments
 (0)