Repository navigation
Expand file tree
/
Copy pathbackground.js
More file actions
439 lines (378 loc) · 13.4 KB
/
Copy pathbackground.js
File metadata and controls
439 lines (378 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
// Background script for General Sticker System (GSS)
// 【TSC 全域快取】避免多個分頁重複請求相同頻道狀態
const statusCache = new Map();
const CACHE_DURATION = 60000; // 1 分鐘快取
const pendingRequests = new Map(); // 進行中的請求去重
// ========= 自定義平台:動態注入 content script =========
const GSS_CONTENT_JS = [
'stickerStorage.js',
'stickerTags.js',
'stickerRegistry.js',
'libraries/base.js',
'libraries/imgur.js',
'libraries/meee.js',
'libraries/catbox.js',
'libraries/index.js',
'platforms/base.js',
'platforms/custom.js',
'platforms/twitch.js',
'platforms/vaughn.js',
'platforms/kick.js',
'platforms/youtube.js',
'platforms/beamstream.js',
'platforms/wtv.js',
'platforms/gosh.js',
'platforms/index.js',
'modules/gsstracker.js',
'TexoStreamCore/sharedChat.js',
'TexoStreamCore/panel.js',
'content.js'
];
function normalizeHostname(input) {
return String(input || '')
.replace(/^https?:\/\//i, '')
.replace(/\/.*$/, '')
.trim();
}
function hostnameToOrigins(hostname) {
const host = normalizeHostname(hostname);
if (!host) return [];
return [
`https://${host}/*`,
`https://*.${host}/*`,
`http://${host}/*`,
`http://*.${host}/*`
];
}
function hostnameToMatches(hostname) {
return hostnameToOrigins(hostname);
}
function scriptIdForHost(hostname) {
return 'gss-custom-' + normalizeHostname(hostname).replace(/[^a-zA-Z0-9]/g, '-');
}
async function registerCustomPlatformScripts(hostname) {
const host = normalizeHostname(hostname);
const matches = hostnameToMatches(host);
if (!matches.length) return { ok: false, error: 'invalid hostname' };
const id = scriptIdForHost(host);
try {
await chrome.scripting.unregisterContentScripts({ ids: [id] });
} catch (e) { /* 首次註冊可能不存在 */ }
await chrome.scripting.registerContentScripts([{
id,
matches,
js: GSS_CONTENT_JS,
allFrames: true,
runAt: 'document_idle'
}]);
return { ok: true, id, host };
}
async function unregisterCustomPlatformScripts(hostname) {
const id = scriptIdForHost(hostname);
try {
await chrome.scripting.unregisterContentScripts({ ids: [id] });
} catch (e) { /* ignore */ }
return { ok: true };
}
async function syncAllCustomPlatformScripts() {
const { customPlatforms } = await chrome.storage.local.get(['customPlatforms']);
const list = Array.isArray(customPlatforms) ? customPlatforms : [];
for (const p of list) {
if (!p?.hostname) continue;
const origins = hostnameToOrigins(p.hostname);
const has = await chrome.permissions.contains({ origins });
if (has) {
await registerCustomPlatformScripts(p.hostname);
}
}
}
// Listen for messages from content scripts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'OPEN_TAB') {
// Open a new tab with the specified URL
chrome.tabs.create({ url: request.url });
sendResponse({ success: true });
return true;
}
if (request.type === 'CHECK_STREAM_STATUS') {
// 在 background 中進行跨域 API 請求
checkStreamStatusInBackground(request.url).then(result => {
sendResponse(result);
}).catch(err => {
console.log('[GSS Background] 檢查失敗:', err.message);
sendResponse({ status: 'unknown', error: err.message });
});
return true; // 保持消息通道開啟
}
if (request.type === 'GSS_REGISTER_CUSTOM_PLATFORM') {
registerCustomPlatformScripts(request.hostname)
.then(sendResponse)
.catch((err) => sendResponse({ ok: false, error: err?.message || String(err) }));
return true;
}
if (request.type === 'GSS_UNREGISTER_CUSTOM_PLATFORM') {
unregisterCustomPlatformScripts(request.hostname)
.then(sendResponse)
.catch((err) => sendResponse({ ok: false, error: err?.message || String(err) }));
return true;
}
});
// 在 background 中檢測開台狀態(解決 CORS 問題 + 全域快取)
async function checkStreamStatusInBackground(url) {
const lowerUrl = url.toLowerCase();
const cacheKey = lowerUrl;
const now = Date.now();
// 1. 檢查快取
const cached = statusCache.get(cacheKey);
if (cached && (now - cached.timestamp < CACHE_DURATION)) {
console.log('[GSS Background] 使用快取:', url, '->', cached.result.status);
return cached.result;
}
// 2. 檢查是否有進行中的相同請求(去重)
if (pendingRequests.has(cacheKey)) {
console.log('[GSS Background] 等待進行中的請求:', url);
return await pendingRequests.get(cacheKey);
}
// 3. 發起新請求並加入 pending
const promise = (async () => {
let result;
if (lowerUrl.includes('twitch.tv')) {
result = await checkTwitchStatus(url);
} else if (lowerUrl.includes('dlive.tv')) {
result = await checkDLiveStatus(url);
} else if (lowerUrl.includes('vaughn.live') || lowerUrl.includes('w.tv')) {
result = await checkVaughnStatus(url);
} else if (lowerUrl.includes('kick.com')) {
result = await checkKickStatus(url);
} else if (lowerUrl.includes('youtube.com')) {
result = await checkYouTubeStatus(url);
} else {
result = { status: 'unknown' };
}
// 存入快取
statusCache.set(cacheKey, { result, timestamp: Date.now() });
pendingRequests.delete(cacheKey);
return result;
})();
pendingRequests.set(cacheKey, promise);
return await promise;
}
// Twitch 狀態檢測(頁面解析法,無需 API 密鑰)
async function checkTwitchStatus(url) {
const match = url.match(/twitch\.tv\/([^\/\?]+)/i);
if (!match) return { status: 'unknown' };
const username = match[1];
try {
const response = await fetch(`https://www.twitch.tv/${username}`, {
method: 'GET',
headers: {
'Accept': 'text/html',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
return { status: 'unknown' };
}
const text = await response.text();
// 檢查開台狀態標記
const hasIsLiveBroadcast = text.includes('"isLiveBroadcast":true');
const hasLiveThumbnail = text.includes(`live_user_${username}`) || text.includes('previews-ttv/live_user_');
const hasJsonLdSchema = text.includes('application/ld+json') && text.includes('VideoObject');
// 如果有任何開台標記,判定為 online
const isLive = hasIsLiveBroadcast || hasLiveThumbnail || (hasJsonLdSchema && hasIsLiveBroadcast);
const status = isLive ? 'online' : 'offline';
return {
status,
details: {
hasIsLiveBroadcast,
hasLiveThumbnail,
hasJsonLdSchema
}
};
} catch (e) {
console.log('[GSS Background] Twitch 頁面解析檢測失敗:', username, e.message);
return { status: 'unknown', error: e.message };
}
}
// DLive 狀態檢測
async function checkDLiveStatus(url) {
const match = url.match(/dlive\.tv\/([^\/\?]+)/i);
if (!match) return { status: 'unknown' };
const username = match[1];
try {
const response = await fetch('https://graphigo.prd.dlive.tv/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: `query { userByDisplayName(displayname: "${username}") { livestream { id } } }`
})
});
const data = await response.json();
const hasLive = data?.data?.userByDisplayName?.livestream?.id;
return { status: hasLive ? 'online' : 'offline' };
} catch (e) {
return { status: 'unknown', error: e.message };
}
}
// Vaughn Live / W.TV 狀態檢測 - 暫時返回 unknown(灰色)
// TODO: 找到可靠的檢測方法後再實作
async function checkVaughnStatus(url) {
const match = url.match(/(?:vaughn\.live|w\.tv)\/([^\/\?]+)/i);
if (!match) return { status: 'unknown' };
const channel = match[1];
console.log('[GSS Background] Vaughn/W.TV 檢測暫時禁用:', channel, '-> 返回 unknown (灰色)');
// 暫時返回 unknown,顯示灰色狀態
// 等找到可靠檢測方法後再實作
return { status: 'unknown' };
}
// 備選方案 1:嘗試普通 CORS 模式
async function checkVaughnStatusWithCors(channel) {
console.log('[GSS Background] 嘗試 CORS 模式檢測 Vaughn:', channel);
try {
const response = await fetch(`https://vaughn.live/app_check.php?channel=${channel}`, {
method: 'GET',
headers: {
'Accept': '*/*'
}
});
const data = await response.text();
const trimmed = data.trim().toLowerCase();
const isLive = trimmed === 'yes' || /^\d+$/.test(trimmed);
return { status: isLive ? 'online' : 'offline' };
} catch (e) {
console.log('[GSS Background] CORS 模式也失敗:', e.message);
// 嘗試縮圖檢測作為最後手段
return await checkVaughnStatusWithThumbnail(channel);
}
}
// 備選方案 3:透過 HLS 串流偵測
async function checkByStream(channel) {
const streamUrl = `https://ms-hls.vaughn.live/live/_definst_/${channel}/playlist.m3u8`;
console.log('[GSS Background] 嘗試 HLS 串流偵測:', channel);
try {
const response = await fetch(streamUrl, {
method: 'HEAD',
cache: 'no-store'
});
if (response.ok) {
console.log(`[GSS Background] ${channel} HLS 偵測: Online`);
return { status: 'online' };
} else {
console.log(`[GSS Background] ${channel} HLS 偵測: Offline (HTTP ${response.status})`);
return { status: 'offline' };
}
} catch (error) {
console.error(`[GSS Background] ${channel} HLS 偵測出錯:`, error);
return { status: 'offline' };
}
}
// 備選方案 2:透過縮圖 URL 檢測(繞過 API CORS)
async function checkVaughnStatusWithThumbnail(channel) {
console.log('[GSS Background] 嘗試縮圖檢測 Vaughn:', channel);
const thumbnailUrl = `https://cdn.vaughn.live/screenshots/${channel}.jpg`;
return new Promise((resolve) => {
const img = new Image();
let timeout;
img.onload = () => {
clearTimeout(timeout);
console.log('[GSS Background] 縮圖載入成功,判定為 online:', channel);
resolve({ status: 'online' });
};
img.onerror = () => {
clearTimeout(timeout);
console.log('[GSS Background] 縮圖載入失敗,判定為 offline:', channel);
// 嘗試 HLS 流檢測作為最後手段
checkByStream(channel).then(resolve);
};
// 設定超時(3秒)
timeout = setTimeout(() => {
console.log('[GSS Background] 縮圖載入超時,嘗試 HLS 檢測:', channel);
checkByStream(channel).then(resolve);
}, 3000);
// 添加隨機參數避免快取
img.src = `${thumbnailUrl}?t=${Date.now()}`;
});
}
// KICK 狀態檢測
// API: https://kick.com/api/v1/channels/{slug}
// 判斷邏輯: livestream 欄位是否為 null
async function checkKickStatus(url) {
const match = url.match(/kick\.com\/([^\/\?]+)/i);
if (!match) return { status: 'unknown' };
const slug = match[1];
try {
console.log('[GSS Background] 檢測 KICK 狀態:', slug);
const response = await fetch(`https://kick.com/api/v1/channels/${slug}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
console.log('[GSS Background] KICK API 回應錯誤:', response.status);
return { status: 'unknown' };
}
const data = await response.json();
// 判斷 livestream 欄位
const isLive = data?.livestream !== null && data?.livestream !== undefined;
const status = isLive ? 'online' : 'offline';
// 如果有縮圖,一起回傳
const thumbnail = data?.livestream?.thumbnail?.url || null;
console.log('[GSS Background] KICK 狀態:', slug, '->', status, thumbnail ? '(有縮圖)' : '');
return { status, thumbnail };
} catch (e) {
console.log('[GSS Background] KICK 檢測失敗:', slug, e.message);
return { status: 'unknown', error: e.message };
}
}
// YouTube 狀態檢測 - 使用頁面解析(免費,無 API 額度限制)
async function checkYouTubeStatus(url) {
try {
// 如果是頻道頁,自動加上 /live 獲取直播頁面
let checkUrl = url;
const channelMatch = url.match(/youtube\.com\/(@[^\/\?]+)/i);
if (channelMatch && !url.includes('/live')) {
checkUrl = `https://www.youtube.com/${channelMatch[1]}/live`;
}
const response = await fetch(checkUrl, {
method: 'GET',
headers: {
'Accept': 'text/html',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
return { status: 'unknown' };
}
const text = await response.text();
// 檢查關鍵字(排除待機室)
const hasIsLive = text.includes('"isLive":true');
const hasIsUpcoming = text.includes('"isUpcoming":true') ||
text.includes('"upcomingEventData"') ||
text.includes('LIVE_BADGE_STYLE_UPCOMING');
const hasLiveBadge = text.includes('LIVE_BADGE_STYLE_LIVE');
// 真正的直播:(isLive=true 且不是待機室) 或有 LIVE 徽章
const isLive = (hasIsLive && !hasIsUpcoming) || hasLiveBadge;
const status = isLive ? 'online' : 'offline';
return {
status,
details: {
isLive: hasIsLive,
isUpcoming: hasIsUpcoming,
hasLiveBadge
}
};
} catch (e) {
return { status: 'unknown', error: e.message };
}
}
// Handle extension install/update
chrome.runtime.onInstalled.addListener(() => {
syncAllCustomPlatformScripts();
});
chrome.runtime.onStartup.addListener(() => {
syncAllCustomPlatformScripts();
});