sonygod commited on
Commit
a55955d
·
1 Parent(s): 14e8f07

基本可用用

Browse files
Files changed (1) hide show
  1. youtube_sub.js +873 -0
youtube_sub.js ADDED
@@ -0,0 +1,873 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==UserScript==
2
+ // @name YouTube Subtitle Manager
3
+ // @namespace http://tampermonkey.net/
4
+ // @version 1.1
5
+ // @description Enhanced YouTube subtitle manager with bilingual support
6
+ // @author Your name
7
+ // @match https://www.youtube.com/*
8
+ // @grant GM_addStyle
9
+ // @run-at document-end
10
+ // ==/UserScript==
11
+
12
+ // Critical z-index style - must be outside IIFE
13
+ GM_addStyle(`
14
+ .subtitle-manager {
15
+ z-index: 9999999 !important;
16
+ }
17
+ `);
18
+ (function () {
19
+ 'use strict';
20
+
21
+ // 增强的样式定义,添加过渡效果
22
+ const styles = `
23
+ .subtitle-manager {
24
+ position: fixed;
25
+ right: 0;
26
+ top: 60px;
27
+ width: 300px;
28
+ height: calc(100vh - 60px);
29
+ background: rgba(33, 33, 33, 0.95);
30
+ color: white;
31
+ z-index: 9999;
32
+ font-family: Arial, sans-serif;
33
+ display: flex;
34
+ flex-direction: column;
35
+ transition: transform 0.3s ease;
36
+ }
37
+
38
+ .subtitle-manager.collapsed {
39
+ transform: translateX(290px);
40
+ }
41
+
42
+ .subtitle-toggle {
43
+ position: absolute;
44
+ left: -30px;
45
+ top: 10px;
46
+ width: 30px;
47
+ height: 30px;
48
+ background: rgba(33, 33, 33, 0.95);
49
+ border: none;
50
+ color: white;
51
+ cursor: pointer;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ border-radius: 4px 0 0 4px;
56
+ }
57
+
58
+ .subtitle-tabs {
59
+ display: flex;
60
+ border-bottom: 1px solid #444;
61
+ }
62
+
63
+ .subtitle-tab {
64
+ flex: 1;
65
+ padding: 10px;
66
+ text-align: center;
67
+ cursor: pointer;
68
+ background: transparent;
69
+ border: none;
70
+ color: white;
71
+ transition: background-color 0.2s ease;
72
+ }
73
+
74
+ .subtitle-tab.active {
75
+ background: #444;
76
+ }
77
+
78
+ .subtitle-content {
79
+ flex: 1;
80
+ overflow-y: auto;
81
+ padding: 10px;
82
+ transition: opacity 0.3s ease;
83
+ }
84
+
85
+ .subtitle-line {
86
+ padding: 8px;
87
+ margin: 4px 0;
88
+ cursor: pointer;
89
+ border-radius: 4px;
90
+ transition: background-color 0.2s ease, transform 0.1s ease;
91
+ position: relative;
92
+ }
93
+
94
+ .subtitle-line:hover {
95
+ background: rgba(255, 255, 255, 0.1);
96
+ transform: translateX(5px);
97
+ }
98
+
99
+ .subtitle-line.active {
100
+ background: rgba(255, 255, 255, 0.2);
101
+ }
102
+
103
+ .mixed-line {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 8px;
107
+ padding: 12px 8px;
108
+ }
109
+
110
+ .en-text {
111
+ color: #fff;
112
+ font-size: 14px;
113
+ }
114
+
115
+ .zh-text {
116
+ color: #aaa;
117
+ font-size: 14px;
118
+ }
119
+
120
+ .timestamp {
121
+ position: absolute;
122
+ right: 8px;
123
+ top: 4px;
124
+ font-size: 10px;
125
+ color: #888;
126
+ opacity: 0;
127
+ transition: opacity 0.2s ease;
128
+ }
129
+
130
+ .subtitle-line:hover .timestamp {
131
+ opacity: 1;
132
+ }
133
+
134
+ /* 自定义滚动条样式 */
135
+ .subtitle-content::-webkit-scrollbar {
136
+ width: 8px;
137
+ }
138
+
139
+ .subtitle-content::-webkit-scrollbar-track {
140
+ background: rgba(255, 255, 255, 0.1);
141
+ }
142
+
143
+ .subtitle-content::-webkit-scrollbar-thumb {
144
+ background: rgba(255, 255, 255, 0.3);
145
+ border-radius: 4px;
146
+ }
147
+ `;
148
+
149
+ // Apply main styles
150
+ GM_addStyle(styles);
151
+ // 添加调试日志函数
152
+ const debug = {
153
+ log: (...args) => {
154
+ console.log('%c[Subtitle Manager]', 'color: #4CAF50', ...args);
155
+ },
156
+ error: (...args) => {
157
+ console.error('%c[Subtitle Manager]', 'color: #f44336', ...args);
158
+ },
159
+ warn: (...args) => {
160
+ console.warn('%c[Subtitle Manager]', 'color: #ff9800', ...args);
161
+ }
162
+ };
163
+
164
+ // 注入样式的函数
165
+ function injectStyles(styles) {
166
+ try {
167
+ const styleSheet = document.createElement('style');
168
+ styleSheet.textContent = styles;
169
+ document.head.appendChild(styleSheet);
170
+ debug.log('Styles injected successfully');
171
+ } catch (error) {
172
+ debug.error('Failed to inject styles:', error);
173
+ }
174
+ }
175
+
176
+ class SubtitleManager {
177
+ constructor() {
178
+ this.currentTab = 'english';
179
+ this.subtitles = {
180
+ english: [],
181
+ chinese: [],
182
+ };
183
+ this.container = null;
184
+ this.currentTime = 0;
185
+ this.isCollapsed = false;
186
+ this.setupUI();
187
+ this.setupEventListeners();
188
+ }
189
+
190
+ checkCaptionSources(player) {
191
+ // Method 1: Direct caption track
192
+ const tracks = player.getElementsByTagName('track');
193
+ if (tracks.length) {
194
+ debug.log('Found caption tracks:', tracks.length);
195
+ return true;
196
+ }
197
+
198
+ // Method 2: YouTube caption button
199
+ const captionButton = document.querySelector('.ytp-subtitles-button');
200
+ if (captionButton) {
201
+ debug.log('Caption button state:', captionButton.getAttribute('aria-pressed'));
202
+ return true;
203
+ }
204
+
205
+ // Method 3: YouTube caption data
206
+ if (player.getPlayerResponse) {
207
+ const response = player.getPlayerResponse();
208
+ debug.log('Player response:', response);
209
+ if (response?.captions?.playerCaptionsTracklistRenderer?.captionTracks) {
210
+ return true;
211
+ }
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ async waitForSubtitles() {
218
+ debug.log('Waiting for subtitles...');
219
+ return new Promise(resolve => {
220
+ const checkSubs = () => {
221
+ const player = document.querySelector('.html5-video-player');
222
+ if (!player) {
223
+ debug.log('No player found, retrying...');
224
+ requestAnimationFrame(checkSubs);
225
+ return;
226
+ }
227
+
228
+ // Check multiple caption sources
229
+ const hasCaptions = this.checkCaptionSources(player);
230
+
231
+ if (hasCaptions) {
232
+ debug.log('Captions found!');
233
+ resolve(true);
234
+ } else {
235
+ debug.log('No captions yet, retrying...');
236
+ requestAnimationFrame(checkSubs);
237
+ }
238
+ };
239
+ checkSubs();
240
+ });
241
+ }
242
+ async waitForYouTubeAPI() {
243
+ debug.log('Waiting for YouTube API initialization...');
244
+ return new Promise(resolve => {
245
+ const check = () => {
246
+ const player = document.querySelector('.html5-video-player');
247
+ debug.log('Checking for player:', player);
248
+ if (player) {
249
+ debug.log('Player API methods:', Object.keys(player));
250
+ }
251
+
252
+ if (window.ytcfg && player) {
253
+ debug.log('YouTube API ready');
254
+ resolve();
255
+ } else {
256
+ requestAnimationFrame(check);
257
+ }
258
+ };
259
+ check();
260
+ });
261
+ }
262
+
263
+ async getVideoTracks() {
264
+ debug.log('Getting video tracks...');
265
+
266
+ // Wait for API
267
+ await this.waitForYouTubeAPI();
268
+
269
+ // Get player with detailed logging
270
+ const player = document.querySelector('.html5-video-player');
271
+ debug.log('Player found:', player);
272
+ debug.log('Player methods:', Object.getOwnPropertyNames(player.__proto__));
273
+
274
+ // Try different methods to get captions
275
+ try {
276
+ // Method 1: Direct API
277
+ if (player.getSubtitlesTrackList) {
278
+ const tracks = await player.getSubtitlesTrackList();
279
+ debug.log('Tracks from API:', tracks);
280
+ return tracks;
281
+ }
282
+
283
+ // Method 2: Get from player config
284
+ if (player.getPlayerResponse) {
285
+ const response = await player.getPlayerResponse();
286
+ debug.log('Player response:', response);
287
+ if (response.captions) {
288
+ return response.captions.playerCaptionsTracklistRenderer.captionTracks;
289
+ }
290
+ }
291
+
292
+ // Method 3: Get from DOM
293
+ const captionButton = document.querySelector('.ytp-subtitles-button');
294
+ debug.log('Caption button found:', captionButton);
295
+ if (captionButton) {
296
+ captionButton.click();
297
+ await new Promise(r => setTimeout(r, 1000));
298
+
299
+ const menu = document.querySelector('.ytp-caption-window-container');
300
+ debug.log('Caption menu found:', menu);
301
+ if (menu) {
302
+ const tracks = Array.from(menu.querySelectorAll('.ytp-caption-track'))
303
+ .map(track => ({
304
+ languageCode: track.dataset.code,
305
+ baseUrl: track.dataset.url
306
+ }));
307
+ debug.log('Tracks from DOM:', tracks);
308
+ return tracks;
309
+ }
310
+ }
311
+
312
+ } catch (error) {
313
+ debug.error('Error getting tracks:', error);
314
+ }
315
+
316
+ debug.warn('No tracks found using any method');
317
+ return [];
318
+ }
319
+
320
+ async loadSubtitles(track) {
321
+ const MAX_RETRIES = 3;
322
+ let retries = 0;
323
+
324
+ while (retries < MAX_RETRIES) {
325
+ try {
326
+ // Use baseUrl instead of src
327
+ const url = new URL(track.baseUrl);
328
+ // Add required params for JSON format
329
+ url.searchParams.set('fmt', 'json3');
330
+
331
+ const response = await fetch(url.toString());
332
+ if (!response.ok) {
333
+ throw new Error(`HTTP error: ${response.status}`);
334
+ }
335
+
336
+ const data = await response.json();
337
+ debug.log('Subtitle data:', data);
338
+
339
+ if (!data?.events) {
340
+ throw new Error('Invalid subtitle data format');
341
+ }
342
+
343
+ const subtitles = data.events
344
+ .filter(event => event.segs && event.tStartMs !== undefined)
345
+ .map(event => ({
346
+ startTime: event.tStartMs / 1000,
347
+ endTime: (event.tStartMs + event.dDurationMs) / 1000,
348
+ text: event.segs.map(seg => seg.utf8).join('').trim()
349
+ }))
350
+ .filter(sub => sub.text);
351
+
352
+ debug.log(`Loaded ${subtitles.length} subtitles`);
353
+ return subtitles;
354
+
355
+ } catch (error) {
356
+ debug.error(`Subtitle load attempt ${retries + 1} failed:`, error);
357
+ retries++;
358
+ if (retries === MAX_RETRIES) {
359
+ debug.error('Failed to load subtitles after all retries');
360
+ return [];
361
+ }
362
+ // Exponential backoff
363
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retries)));
364
+ }
365
+ }
366
+ return [];
367
+ }
368
+
369
+ parseVTT(vttText) {
370
+ const lines = vttText.trim().split('\n');
371
+ const subtitles = [];
372
+ let currentSubtitle = null;
373
+
374
+ for (let i = 0; i < lines.length; i++) {
375
+ const line = lines[i].trim();
376
+
377
+ // 跳过 WEBVTT 头部
378
+ if (line === 'WEBVTT' || line === '') continue;
379
+
380
+ // 解析时间戳
381
+ const timeMatch = line.match(/(\d{2}):(\d{2}):(\d{2})\.(\d{3}) --> (\d{2}):(\d{2}):(\d{2})\.(\d{3})/);
382
+ if (timeMatch) {
383
+ if (currentSubtitle) {
384
+ subtitles.push(currentSubtitle);
385
+ }
386
+
387
+ currentSubtitle = {
388
+ startTime: this.parseTime(timeMatch.slice(1, 5)),
389
+ endTime: this.parseTime(timeMatch.slice(5, 9)),
390
+ text: ''
391
+ };
392
+ continue;
393
+ }
394
+
395
+ // 收集字幕文本
396
+ if (currentSubtitle && line) {
397
+ currentSubtitle.text += (currentSubtitle.text ? '\n' : '') + line;
398
+ }
399
+ }
400
+
401
+ // 添加最后一条字幕
402
+ if (currentSubtitle) {
403
+ subtitles.push(currentSubtitle);
404
+ }
405
+
406
+ return subtitles;
407
+ }
408
+
409
+ parseTime(timeParts) {
410
+ return parseInt(timeParts[0]) * 3600 +
411
+ parseInt(timeParts[1]) * 60 +
412
+ parseInt(timeParts[2]) +
413
+ parseInt(timeParts[3]) / 1000;
414
+ }
415
+
416
+ formatTime(seconds) {
417
+ const h = Math.floor(seconds / 3600);
418
+ const m = Math.floor((seconds % 3600) / 60);
419
+ const s = Math.floor(seconds % 60);
420
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
421
+ }
422
+
423
+ async waitForElement(selector, timeout = 10000) {
424
+ debug.log(`Waiting for element: ${selector}`);
425
+ const element = document.querySelector(selector);
426
+ if (element) return element;
427
+
428
+ return new Promise(resolve => {
429
+ const observer = new MutationObserver(() => {
430
+ const element = document.querySelector(selector);
431
+ if (element) {
432
+ observer.disconnect();
433
+ resolve(element);
434
+ }
435
+ });
436
+
437
+ observer.observe(document.body, {
438
+ childList: true,
439
+ subtree: true
440
+ });
441
+ });
442
+ }
443
+
444
+ setupUI() {
445
+ debug.log('Setting up UI');
446
+ try {
447
+ // Create container with trusted content
448
+ this.container = document.createElement('div');
449
+ this.container.className = 'subtitle-manager';
450
+
451
+ // Create toggle button
452
+ const toggleButton = document.createElement('button');
453
+ toggleButton.className = 'subtitle-toggle';
454
+ toggleButton.textContent = '≡'; // Use textContent instead of innerHTML
455
+
456
+ // Create tabs container
457
+ const tabs = document.createElement('div');
458
+ tabs.className = 'subtitle-tabs';
459
+
460
+ // Create tab buttons safely
461
+ const tabsData = [
462
+ { id: 'english', text: 'English' },
463
+ { id: 'chinese', text: '中文' },
464
+ { id: 'mixed', text: 'Mixed' }
465
+ ];
466
+
467
+ tabsData.forEach(tab => {
468
+ const button = document.createElement('button');
469
+ button.className = 'subtitle-tab';
470
+ button.dataset.tab = tab.id;
471
+ button.textContent = tab.text;
472
+ if (tab.id === 'english') button.classList.add('active');
473
+ tabs.appendChild(button);
474
+ });
475
+
476
+ // Create content container
477
+ const content = document.createElement('div');
478
+ content.className = 'subtitle-content';
479
+
480
+ // Append elements
481
+ this.container.appendChild(toggleButton);
482
+ this.container.appendChild(tabs);
483
+ this.container.appendChild(content);
484
+
485
+ // Wait for document.body to be available
486
+ if (document.body) {
487
+ document.body.appendChild(this.container);
488
+ } else {
489
+ window.addEventListener('DOMContentLoaded', () => {
490
+ document.body.appendChild(this.container);
491
+ });
492
+ }
493
+
494
+ // Add toggle functionality
495
+ toggleButton.addEventListener('click', () => this.toggleCollapse());
496
+
497
+ } catch (error) {
498
+ debug.error('Error setting up UI:', error);
499
+ }
500
+ }
501
+
502
+ toggleCollapse() {
503
+ this.isCollapsed = !this.isCollapsed;
504
+ this.container.classList.toggle('collapsed', this.isCollapsed);
505
+ }
506
+
507
+ displaySingleLanguage(container, subtitles) {
508
+ subtitles.forEach((sub, index) => {
509
+ const line = document.createElement('div');
510
+ line.className = 'subtitle-line';
511
+
512
+ const text = document.createElement('span');
513
+ text.textContent = sub.text;
514
+
515
+ const timestamp = document.createElement('span');
516
+ timestamp.className = 'timestamp';
517
+ timestamp.textContent = this.formatTime(sub.startTime);
518
+
519
+ line.appendChild(text);
520
+ line.appendChild(timestamp);
521
+ line.dataset.time = sub.startTime;
522
+ line.dataset.index = index;
523
+
524
+ container.appendChild(line);
525
+ });
526
+ }
527
+
528
+ displayMixed(container) {
529
+ const maxLength = Math.max(
530
+ this.subtitles.english.length,
531
+ this.subtitles.chinese.length
532
+ );
533
+
534
+ for (let i = 0; i < maxLength; i++) {
535
+ const line = document.createElement('div');
536
+ line.className = 'subtitle-line mixed-line';
537
+
538
+ const enSub = this.subtitles.english[i];
539
+ const zhSub = this.subtitles.chinese[i];
540
+
541
+ const enText = document.createElement('div');
542
+ enText.className = 'en-text';
543
+ enText.textContent = enSub?.text || '';
544
+
545
+ const zhText = document.createElement('div');
546
+ zhText.className = 'zh-text';
547
+ zhText.textContent = zhSub?.text || '';
548
+
549
+ const timestamp = document.createElement('span');
550
+ timestamp.className = 'timestamp';
551
+ timestamp.textContent = enSub ? this.formatTime(enSub.startTime) : '';
552
+
553
+ line.appendChild(enText);
554
+ line.appendChild(zhText);
555
+ line.appendChild(timestamp);
556
+
557
+ if (enSub) {
558
+ line.dataset.time = enSub.startTime;
559
+ line.dataset.index = i;
560
+ }
561
+
562
+ container.appendChild(line);
563
+ }
564
+ }
565
+
566
+ setupEventListeners() {
567
+ // 标签切换
568
+ this.container.addEventListener('click', (e) => {
569
+ if (e.target.classList.contains('subtitle-tab')) {
570
+ const tabs = this.container.querySelectorAll('.subtitle-tab');
571
+ tabs.forEach(tab => tab.classList.remove('active'));
572
+ e.target.classList.add('active');
573
+ this.currentTab = e.target.dataset.tab;
574
+ this.updateSubtitleDisplay();
575
+ }
576
+ });
577
+
578
+ // 字幕点击跳转
579
+ this.container.addEventListener('click', (e) => {
580
+ const line = e.target.closest('.subtitle-line');
581
+ if (line && line.dataset.time) {
582
+ const video = document.querySelector('video');
583
+ if (video) {
584
+ video.currentTime = parseFloat(line.dataset.time);
585
+ // 添加点击反馈
586
+ line.style.transform = 'scale(0.98)';
587
+ setTimeout(() => {
588
+ line.style.transform = '';
589
+ }, 100);
590
+ }
591
+ }
592
+ });
593
+
594
+ // Tab 键切换字幕
595
+ document.addEventListener('keydown', (e) => {
596
+ if (e.key === 'Tab') {
597
+ e.preventDefault();
598
+ const tabs = ['english', 'chinese', 'mixed'];
599
+ const currentIndex = tabs.indexOf(this.currentTab);
600
+ const nextIndex = (currentIndex + 1) % tabs.length;
601
+ this.currentTab = tabs[nextIndex];
602
+
603
+ const tabButtons = this.container.querySelectorAll('.subtitle-tab');
604
+ tabButtons.forEach(tab => {
605
+ tab.classList.toggle('active', tab.dataset.tab === this.currentTab);
606
+ });
607
+
608
+ this.updateSubtitleDisplay();
609
+ }
610
+ });
611
+
612
+ // 视频时间更新
613
+ this.setupVideoTimeUpdate();
614
+ }
615
+
616
+ setupVideoTimeUpdate() {
617
+ let rafId = null;
618
+ const updateThreshold = 16; // ~60fps
619
+ let lastUpdate = 0;
620
+
621
+ const findSubtitleIndex = (time, subtitles) => {
622
+ // Binary search for faster lookup
623
+ let start = 0;
624
+ let end = subtitles.length - 1;
625
+
626
+ while (start <= end) {
627
+ const mid = Math.floor((start + end) / 2);
628
+ const sub = subtitles[mid];
629
+
630
+ if (time >= sub.startTime && time <= sub.endTime) {
631
+ return mid;
632
+ }
633
+
634
+ if (time < sub.startTime) {
635
+ end = mid - 1;
636
+ } else {
637
+ start = mid + 1;
638
+ }
639
+ }
640
+ return -1;
641
+ };
642
+
643
+ const handleTimeUpdate = (video) => {
644
+ const now = performance.now();
645
+ if (now - lastUpdate < updateThreshold) {
646
+ rafId = requestAnimationFrame(() => handleTimeUpdate(video));
647
+ return;
648
+ }
649
+ lastUpdate = now;
650
+
651
+ const currentTime = video.currentTime;
652
+ const subtitles = this.currentTab === 'chinese' ?
653
+ this.subtitles.chinese : this.subtitles.english;
654
+
655
+ const newIndex = findSubtitleIndex(currentTime, subtitles);
656
+ if (newIndex === this.subtitleIndex) {
657
+ rafId = requestAnimationFrame(() => handleTimeUpdate(video));
658
+ return;
659
+ }
660
+
661
+ this.subtitleIndex = newIndex;
662
+ const lines = this.container.querySelectorAll('.subtitle-line');
663
+
664
+ lines.forEach(line => {
665
+ const lineIndex = parseInt(line.dataset.index);
666
+ const isActive = lineIndex === newIndex;
667
+
668
+ line.classList.toggle('active', isActive);
669
+ if (isActive) {
670
+ line.scrollIntoView({
671
+ block: 'center',
672
+ behavior: 'auto'
673
+ });
674
+ }
675
+ });
676
+
677
+ rafId = requestAnimationFrame(() => handleTimeUpdate(video));
678
+ };
679
+
680
+ const setupVideoListener = async () => {
681
+ const video = await this.waitForElement('video');
682
+ if (video) {
683
+ handleTimeUpdate(video);
684
+
685
+ video.addEventListener('pause', () => {
686
+ cancelAnimationFrame(rafId);
687
+ });
688
+
689
+ video.addEventListener('play', () => {
690
+ lastUpdate = 0;
691
+ handleTimeUpdate(video);
692
+ });
693
+ }
694
+ };
695
+
696
+ setupVideoListener();
697
+ }
698
+
699
+ async getCaptionTracks() {
700
+ const player = document.querySelector('.html5-video-player');
701
+ if (!player) return null;
702
+
703
+ try {
704
+ // Try YouTube's API first
705
+ if (player.getPlayerResponse) {
706
+ const response = player.getPlayerResponse();
707
+ if (response?.captions?.playerCaptionsTracklistRenderer?.captionTracks) {
708
+ return response.captions.playerCaptionsTracklistRenderer.captionTracks;
709
+ }
710
+ }
711
+
712
+ // Fallback to DOM tracks
713
+ return Array.from(player.getElementsByTagName('track'));
714
+
715
+ } catch (error) {
716
+ debug.error('Error getting caption tracks:', error);
717
+ return null;
718
+ }
719
+ }
720
+
721
+ async initializeSubtitles() {
722
+ debug.log('Starting subtitle initialization');
723
+
724
+ // 1. Wait for subtitles
725
+ const hasCaptions = await this.waitForSubtitles();
726
+ if (!hasCaptions) {
727
+ debug.error('No captions available');
728
+ return;
729
+ }
730
+
731
+ // 2. Get tracks
732
+ const tracks = await this.getCaptionTracks();
733
+ if (!tracks?.length) {
734
+ debug.error('No caption tracks found');
735
+ return;
736
+ }
737
+
738
+ //enter debug mode
739
+
740
+ //debugger;
741
+
742
+ // 3. Load subtitle data
743
+ for (const track of tracks) {
744
+ if (track.languageCode.includes('en')) {
745
+ this.subtitles.english = await this.loadSubtitles(track);
746
+ }
747
+ if (track.languageCode.includes('zh')) {
748
+ this.subtitles.chinese = await this.loadSubtitles(track);
749
+ }
750
+ }
751
+
752
+ // 4. Display subtitles
753
+ this.updateSubtitleDisplay();
754
+ }
755
+ createMessageElement(text) {
756
+ const div = document.createElement('div');
757
+ div.className = 'subtitle-line';
758
+ div.textContent = text;
759
+ return div;
760
+ }
761
+
762
+ updateSubtitleDisplay() {
763
+ const content = this.container.querySelector('.subtitle-content');
764
+ content.style.opacity = '0';
765
+
766
+ setTimeout(() => {
767
+ // Clear content safely
768
+ while (content.firstChild) {
769
+ content.removeChild(content.firstChild);
770
+ }
771
+
772
+ // Check for empty subtitles
773
+ if (!this.subtitles.english.length && !this.subtitles.chinese.length) {
774
+ content.appendChild(this.createMessageElement('No subtitles available'));
775
+ content.style.opacity = '1';
776
+ return;
777
+ }
778
+
779
+ // Display based on current tab
780
+ switch (this.currentTab) {
781
+ case 'english':
782
+ if (this.subtitles.english.length) {
783
+ this.displaySingleLanguage(content, this.subtitles.english);
784
+ } else {
785
+ content.appendChild(this.createMessageElement('English subtitles not available'));
786
+ }
787
+ break;
788
+ case 'chinese':
789
+ if (this.subtitles.chinese.length) {
790
+ this.displaySingleLanguage(content, this.subtitles.chinese);
791
+ } else {
792
+ content.appendChild(this.createMessageElement('中文字幕不可用'));
793
+ }
794
+ break;
795
+ case 'mixed':
796
+ if (this.subtitles.english.length || this.subtitles.chinese.length) {
797
+ this.displayMixed(content);
798
+ } else {
799
+ content.appendChild(this.createMessageElement('No subtitles available for mixed mode'));
800
+ }
801
+ break;
802
+ }
803
+
804
+ // Fade in
805
+ content.style.opacity = '1';
806
+ }, 300);
807
+ }
808
+ }
809
+
810
+ // Modified initialization with proper waiting
811
+ async function waitForYouTubeReady() {
812
+ debug.log('Waiting for YouTube player...');
813
+ return new Promise((resolve) => {
814
+ const check = () => {
815
+ const player = document.querySelector('.html5-video-player');
816
+ const video = document.querySelector('video');
817
+ if (player && video && !player.classList.contains('uninitialized')) {
818
+ debug.log('YouTube player ready');
819
+ resolve();
820
+ } else {
821
+ requestAnimationFrame(check);
822
+ }
823
+ };
824
+ check();
825
+ });
826
+ }
827
+
828
+ // Add init logging
829
+ async function initializeManager() {
830
+ debug.log('=== Starting initialization ===');
831
+ debug.log('Current URL:', window.location.href);
832
+ debug.log('Document ready state:', document.readyState);
833
+
834
+ if (!window.location.pathname.includes('/watch')) {
835
+ debug.log('Not a video page, skipping');
836
+ return;
837
+ }
838
+
839
+ try {
840
+ await waitForYouTubeReady();
841
+ debug.log('YouTube ready, creating manager');
842
+ const manager = new SubtitleManager();
843
+
844
+ debug.log('Manager created, initializing subtitles');
845
+ await manager.initializeSubtitles();
846
+ debug.log('=== Initialization complete ===');
847
+ } catch (error) {
848
+ debug.error('Initialization failed:', error);
849
+ }
850
+ }
851
+ // Modified URL change detection
852
+ let lastUrl = location.href;
853
+ const observer = new MutationObserver(() => {
854
+ if (location.href !== lastUrl) {
855
+ lastUrl = location.href;
856
+ initializeManager();
857
+ }
858
+ });
859
+
860
+ // Start observing
861
+ observer.observe(document.body, {
862
+ subtree: true,
863
+ childList: true
864
+ });
865
+
866
+ // Initialize when document is ready
867
+ if (document.readyState === 'loading') {
868
+ document.addEventListener('DOMContentLoaded', initializeManager);
869
+ } else {
870
+ setTimeout(initializeManager, 2000);
871
+ }
872
+ })();
873
+ //