myetv-player 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/css/myetv-player.css +131 -0
  2. package/css/myetv-player.min.css +1 -1
  3. package/dist/myetv-player.js +547 -102
  4. package/dist/myetv-player.min.js +486 -93
  5. package/package.json +35 -17
  6. package/plugins/twitch/myetv-player-twitch-plugin.js +125 -11
  7. package/plugins/vimeo/myetv-player-vimeo.js +80 -49
  8. package/plugins/youtube/README.md +5 -2
  9. package/plugins/youtube/myetv-player-youtube-plugin.js +766 -6
  10. package/.github/workflows/codeql.yml +0 -100
  11. package/.github/workflows/npm-publish.yml +0 -30
  12. package/SECURITY.md +0 -50
  13. package/build.js +0 -195
  14. package/scss/README.md +0 -161
  15. package/scss/_audio-player.scss +0 -21
  16. package/scss/_base.scss +0 -116
  17. package/scss/_controls.scss +0 -204
  18. package/scss/_loading.scss +0 -111
  19. package/scss/_menus.scss +0 -432
  20. package/scss/_mixins.scss +0 -112
  21. package/scss/_poster.scss +0 -8
  22. package/scss/_progress-bar.scss +0 -319
  23. package/scss/_resolution.scss +0 -68
  24. package/scss/_responsive.scss +0 -1368
  25. package/scss/_themes.scss +0 -30
  26. package/scss/_title-overlay.scss +0 -60
  27. package/scss/_tooltips.scss +0 -7
  28. package/scss/_variables.scss +0 -49
  29. package/scss/_video.scss +0 -221
  30. package/scss/_volume.scss +0 -122
  31. package/scss/_watermark.scss +0 -128
  32. package/scss/myetv-player.scss +0 -51
  33. package/scss/package.json +0 -16
  34. package/src/README.md +0 -560
  35. package/src/chapters.js +0 -521
  36. package/src/controls.js +0 -1242
  37. package/src/core.js +0 -1922
  38. package/src/events.js +0 -537
  39. package/src/fullscreen.js +0 -82
  40. package/src/i18n.js +0 -374
  41. package/src/playlist.js +0 -177
  42. package/src/plugins.js +0 -384
  43. package/src/quality.js +0 -963
  44. package/src/streaming.js +0 -346
  45. package/src/subtitles.js +0 -524
  46. package/src/utils.js +0 -65
  47. package/src/watermark.js +0 -246
@@ -35,6 +35,9 @@
35
35
  // Enable or disable click over youtube player
36
36
  mouseClick: options.mouseClick !== undefined ? options.mouseClick : false,
37
37
 
38
+ // Show YouTube chapters in the timeline
39
+ showYoutubeChapters: options.showYoutubeChapters !== undefined ? options.showYoutubeChapters : true,
40
+
38
41
  debug: true,
39
42
  ...options
40
43
  };
@@ -67,6 +70,9 @@
67
70
  this.resizeListenerAdded = false;
68
71
  // Channel data cache
69
72
  this.channelData = null;
73
+ // Chapters cache in localStorage
74
+ this.cacheExpiration = 21600000; // 6 hour in milliseconds
75
+ this.cachePrefix = 'myetv_yt_chapters_'; // prefix for localStorage keys
70
76
  //live streaming
71
77
  this.isLiveStream = false;
72
78
  this.liveCheckInterval = null;
@@ -1388,6 +1394,18 @@
1388
1394
  this.updatePlayerWatermark();
1389
1395
  }
1390
1396
 
1397
+ // Load and display chapters if API key is available
1398
+ if (this.options.apiKey) {
1399
+ this.fetchVideoChapters(this.videoId).then(chapters => {
1400
+ if (chapters) {
1401
+ this.displayChaptersOnProgressBar(chapters);
1402
+ }
1403
+ });
1404
+ }
1405
+
1406
+ // Clear expired cache entries on player ready
1407
+ this.clearExpiredCache();
1408
+
1391
1409
  // Set auto caption language
1392
1410
  if (this.options.autoCaptionLanguage) {
1393
1411
  setTimeout(() => this.setAutoCaptionLanguage(), 1500);
@@ -2077,15 +2095,58 @@
2077
2095
  document.head.appendChild(style);
2078
2096
  this.api.container.classList.add('youtube-active');
2079
2097
 
2098
+ // Add chapter styles
2099
+ const chapterStyles = `
2100
+ /* Chapter segments styling */
2101
+ .chapter-segment {
2102
+ box-sizing: border-box;
2103
+ }
2104
+
2105
+ /* Make progress fill respect chapter segments */
2106
+ .progress-filled {
2107
+ z-index: 5 !important;
2108
+ pointer-events: none;
2109
+ }
2110
+
2111
+ /* Ensure chapter markers are visible */
2112
+ .chapter-marker {
2113
+ z-index: 6 !important;
2114
+ }
2115
+
2116
+ /* Enhanced tooltip */
2117
+ .yt-seek-tooltip {
2118
+ animation: fadeIn 0.15s ease-in-out;
2119
+ }
2120
+
2121
+ @keyframes fadeIn {
2122
+ from {
2123
+ opacity: 0;
2124
+ transform: translateX(-50%) translateY(-5px);
2125
+ }
2126
+ to {
2127
+ opacity: 1;
2128
+ transform: translateX(-50%) translateY(0);
2129
+ }
2130
+ }
2131
+
2132
+ /* Hover effect on progress container */
2133
+ .progress-container:hover .chapter-segment {
2134
+ background: rgba(255, 255, 255, 0.4) !important;
2135
+ }
2136
+ `;
2137
+
2138
+ style.textContent = chapterStyles;
2139
+ document.head.appendChild(style);
2140
+
2080
2141
  if (this.api.player.options.debug) {
2081
2142
  console.log('YT Plugin: CSS override injected');
2082
2143
  }
2083
2144
  }
2084
2145
 
2085
2146
  /**
2086
- * Inject CSS styles for controlbar and title overlay gradients
2087
- * Uses YouTube-specific selectors to avoid conflicts with other plugins
2088
- */
2147
+ * Inject CSS styles for controlbar and title overlay gradients
2148
+ * Uses YouTube-specific selectors to avoid conflicts with other plugins
2149
+ */
2089
2150
  injectControlbarGradientStyles() {
2090
2151
  // Check if styles are already injected
2091
2152
  if (document.getElementById('yt-controlbar-gradient-styles')) {
@@ -4205,6 +4266,706 @@
4205
4266
  }
4206
4267
  }
4207
4268
 
4269
+ /**
4270
+ * Fetch video chapters from description using YouTube Data API v3
4271
+ * Uses localStorage for persistent caching
4272
+ */
4273
+ async fetchVideoChapters(videoId) {
4274
+
4275
+ // Check if chapters display is enabled
4276
+ if (!this.options.showYoutubeChapters) {
4277
+ if (this.api.player.options.debug) {
4278
+ console.log('YT Plugin: YouTube chapters display disabled by option');
4279
+ }
4280
+ return null;
4281
+ }
4282
+
4283
+ // Check if API key is available
4284
+ if (!this.options.apiKey) {
4285
+ if (this.api.player.options.debug) {
4286
+ console.warn('YT Plugin: API Key required to fetch chapters');
4287
+ }
4288
+ return null;
4289
+ }
4290
+
4291
+ // Check cache first (localStorage)
4292
+ const cached = this.getCachedChapters(videoId);
4293
+ if (cached) {
4294
+ if (this.api.player.options.debug) {
4295
+ console.log('YT Plugin: Using cached chapters for', videoId);
4296
+ }
4297
+ return cached;
4298
+ }
4299
+
4300
+ try {
4301
+ const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.options.apiKey}`;
4302
+ const response = await fetch(url);
4303
+ const data = await response.json();
4304
+
4305
+ if (!data.items || data.items.length === 0) {
4306
+ // Cache null result too to avoid repeated calls
4307
+ this.setCachedChapters(videoId, null);
4308
+ return null;
4309
+ }
4310
+
4311
+ const description = data.items[0].snippet.description;
4312
+ const chapters = this.parseChaptersFromDescription(description);
4313
+
4314
+ // Cache the result (even if null)
4315
+ this.setCachedChapters(videoId, chapters);
4316
+
4317
+ if (this.api.player.options.debug) {
4318
+ console.log('YT Plugin: Chapters fetched and cached:', chapters);
4319
+ }
4320
+
4321
+ return chapters;
4322
+ } catch (error) {
4323
+ if (this.api.player.options.debug) {
4324
+ console.error('YT Plugin: Error fetching chapters', error);
4325
+ }
4326
+ return null;
4327
+ }
4328
+ }
4329
+
4330
+ /**
4331
+ * Get cached chapters from localStorage
4332
+ */
4333
+ getCachedChapters(videoId) {
4334
+ try {
4335
+ const key = this.cachePrefix + videoId;
4336
+ const cached = localStorage.getItem(key);
4337
+
4338
+ if (!cached) return null;
4339
+
4340
+ const data = JSON.parse(cached);
4341
+ const now = Date.now();
4342
+
4343
+ // Check if cache is expired
4344
+ if (now - data.timestamp >= this.cacheExpiration) {
4345
+ localStorage.removeItem(key);
4346
+ if (this.api.player.options.debug) {
4347
+ console.log('YT Plugin: Cache expired for', videoId);
4348
+ }
4349
+ return null;
4350
+ }
4351
+
4352
+ return data.chapters;
4353
+ } catch (error) {
4354
+ if (this.api.player.options.debug) {
4355
+ console.error('YT Plugin: Error reading cache', error);
4356
+ }
4357
+ return null;
4358
+ }
4359
+ }
4360
+
4361
+ /**
4362
+ * Set cached chapters in localStorage
4363
+ */
4364
+ setCachedChapters(videoId, chapters) {
4365
+ try {
4366
+ const key = this.cachePrefix + videoId;
4367
+ const data = {
4368
+ chapters: chapters,
4369
+ timestamp: Date.now()
4370
+ };
4371
+ localStorage.setItem(key, JSON.stringify(data));
4372
+
4373
+ if (this.api.player.options.debug) {
4374
+ console.log('YT Plugin: Chapters cached for', videoId);
4375
+ }
4376
+ } catch (error) {
4377
+ if (this.api.player.options.debug) {
4378
+ console.error('YT Plugin: Error writing cache', error);
4379
+ }
4380
+ }
4381
+ }
4382
+
4383
+ /**
4384
+ * Parse chapters from video description
4385
+ * Validates YouTube chapter requirements: 3+ chapters, starts at 0:00, 10+ seconds each
4386
+ */
4387
+ parseChaptersFromDescription(description) {
4388
+ if (!description) return null;
4389
+
4390
+ const chapters = [];
4391
+ // Regex for timestamps: 0:00, 00:00, 0:00:00
4392
+ // Matches both "0:00 Title" and "0:00 - Title"
4393
+ const timestampRegex = /(?:^|\n)(\d{1,2}:?\d{0,2}:?\d{2})\s*[-–—]?\s*(.+?)(?=\n|$)/gm;
4394
+ let match;
4395
+
4396
+ while ((match = timestampRegex.exec(description)) !== null) {
4397
+ const timeString = match[1].trim();
4398
+ const title = match[2].trim();
4399
+
4400
+ // Skip if title is too short or empty
4401
+ if (!title || title.length < 2) continue;
4402
+
4403
+ const seconds = this.parseTimeToSeconds(timeString);
4404
+
4405
+ if (seconds !== null) {
4406
+ chapters.push({
4407
+ time: seconds,
4408
+ title: title,
4409
+ timeString: timeString
4410
+ });
4411
+ }
4412
+ }
4413
+
4414
+ // Sort by time
4415
+ chapters.sort((a, b) => a.time - b.time);
4416
+
4417
+ // Validate: at least 3 chapters and first starts at 0:00
4418
+ if (chapters.length >= 3 && chapters[0].time === 0) {
4419
+ // Validate minimum duration (10 seconds)
4420
+ let valid = true;
4421
+ for (let i = 0; i < chapters.length - 1; i++) {
4422
+ if (chapters[i + 1].time - chapters[i].time < 10) {
4423
+ valid = false;
4424
+ break;
4425
+ }
4426
+ }
4427
+
4428
+ if (valid) {
4429
+ if (this.api.player.options.debug) {
4430
+ console.log(`YT Plugin: Found ${chapters.length} valid chapters`);
4431
+ }
4432
+ return chapters;
4433
+ }
4434
+ }
4435
+
4436
+ if (this.api.player.options.debug) {
4437
+ console.log('YT Plugin: No valid chapters found (requires 3+ chapters, starting at 0:00, each 10+ seconds)');
4438
+ }
4439
+
4440
+ return null;
4441
+ }
4442
+
4443
+ /**
4444
+ * Parse time string to seconds
4445
+ */
4446
+ parseTimeToSeconds(timeString) {
4447
+ const parts = timeString.split(':').map(p => parseInt(p, 10));
4448
+
4449
+ if (parts.some(p => isNaN(p))) return null;
4450
+
4451
+ if (parts.length === 2) {
4452
+ // mm:ss
4453
+ return parts[0] * 60 + parts[1];
4454
+ } else if (parts.length === 3) {
4455
+ // hh:mm:ss
4456
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
4457
+ }
4458
+
4459
+ return null;
4460
+ }
4461
+
4462
+ /**
4463
+ * Display chapter markers on progress bar with YouTube-style segments
4464
+ */
4465
+ displayChaptersOnProgressBar(chapters) {
4466
+
4467
+ // Check if chapters display is enabled
4468
+ if (!this.options.showYoutubeChapters) {
4469
+ if (this.api.player.options.debug) {
4470
+ console.log('YT Plugin: YouTube chapters display disabled');
4471
+ }
4472
+ return;
4473
+ }
4474
+
4475
+ if (!chapters || chapters.length === 0) return;
4476
+
4477
+ const progressContainer = this.api.container.querySelector('.progress-container');
4478
+ if (!progressContainer) return;
4479
+
4480
+ const duration = this.ytPlayer.getDuration();
4481
+ if (!duration || duration === 0) return;
4482
+
4483
+ // Store chapters for tooltip display
4484
+ this.chapters = chapters;
4485
+
4486
+ // Remove existing chapter markers and segments
4487
+ progressContainer.querySelectorAll('.chapter-marker, .chapter-segment').forEach(m => m.remove());
4488
+
4489
+ // Create segments for each chapter (physically divided)
4490
+ chapters.forEach((chapter, index) => {
4491
+ const nextChapter = chapters[index + 1];
4492
+ const startPercent = (chapter.time / duration) * 100;
4493
+ const endPercent = nextChapter ? (nextChapter.time / duration) * 100 : 100;
4494
+
4495
+ // Calculate segment width minus the gap
4496
+ const gapSize = nextChapter ? 6 : 0; // 6px gap between segments
4497
+ const widthPercent = endPercent - startPercent;
4498
+
4499
+ // Create segment container
4500
+ const segment = document.createElement('div');
4501
+ segment.className = 'chapter-segment';
4502
+ segment.style.cssText = `
4503
+ position: absolute;
4504
+ left: ${startPercent}%;
4505
+ top: 0;
4506
+ width: calc(${widthPercent}% - ${gapSize}px);
4507
+ height: 100%;
4508
+ background: rgba(255, 255, 255, 0.3);
4509
+ cursor: pointer;
4510
+ z-index: 3;
4511
+ transition: background 0.2s;
4512
+ pointer-events: none;
4513
+ `;
4514
+
4515
+ segment.dataset.chapterIndex = index;
4516
+ segment.dataset.time = chapter.time;
4517
+ segment.dataset.title = chapter.title;
4518
+
4519
+ progressContainer.appendChild(segment);
4520
+
4521
+ // Add marker at the START of next segment (not between)
4522
+ if (nextChapter) {
4523
+ const marker = document.createElement('div');
4524
+ marker.className = 'chapter-marker';
4525
+ marker.style.cssText = `
4526
+ position: absolute !important;
4527
+ left: ${endPercent}% !important;
4528
+ top: 0 !important;
4529
+ width: 6px !important;
4530
+ height: 100% !important;
4531
+ background: transparent !important;
4532
+ border: none !important;
4533
+ box-shadow: none !important;
4534
+ margin-left: -3px !important;
4535
+ cursor: pointer !important;
4536
+ z-index: 10 !important;
4537
+ `;
4538
+
4539
+ marker.dataset.chapterTime = nextChapter.time;
4540
+ marker.dataset.chapterTitle = nextChapter.title;
4541
+
4542
+ // Click on marker to jump to chapter start
4543
+ marker.addEventListener('click', (e) => {
4544
+ e.stopPropagation();
4545
+ this.ytPlayer.seekTo(nextChapter.time, true);
4546
+
4547
+ if (this.api.player.options.debug) {
4548
+ console.log(`YT Plugin: Jumped to chapter "${nextChapter.title}" at ${nextChapter.timeString}`);
4549
+ }
4550
+ });
4551
+
4552
+ progressContainer.appendChild(marker);
4553
+ }
4554
+ });
4555
+
4556
+ if (this.api.player.options.debug) {
4557
+ console.log(`YT Plugin: ${chapters.length} chapter segments displayed with gaps`);
4558
+ }
4559
+
4560
+ // Initialize chapter display in title overlay
4561
+ this.initChapterDisplayInOverlay();
4562
+
4563
+ // Add chapter title tooltip
4564
+ this.addChapterTooltip();
4565
+ }
4566
+
4567
+ /**
4568
+ * Add chapter title tooltip with edge detection
4569
+ */
4570
+ addChapterTooltip() {
4571
+
4572
+ // Check if chapters display is enabled
4573
+ if (!this.options.showYoutubeChapters) {
4574
+ if (this.api.player.options.debug) {
4575
+ console.log('YT Plugin: Chapter tooltip display disabled');
4576
+ }
4577
+ return;
4578
+ }
4579
+
4580
+ if (!this.chapters || this.chapters.length === 0) return;
4581
+
4582
+ const progressContainer = this.api.container.querySelector('.progress-container');
4583
+ if (!progressContainer) return;
4584
+
4585
+ // Remove existing chapter tooltip
4586
+ let chapterTooltip = progressContainer.querySelector('.yt-chapter-tooltip');
4587
+ if (chapterTooltip) {
4588
+ chapterTooltip.remove();
4589
+ }
4590
+
4591
+ // Create chapter tooltip
4592
+ chapterTooltip = document.createElement('div');
4593
+ chapterTooltip.className = 'yt-chapter-tooltip';
4594
+ chapterTooltip.style.cssText = `
4595
+ position: absolute;
4596
+ bottom: calc(100% + 35px);
4597
+ left: 0;
4598
+ background: rgba(28, 28, 28, 0.95);
4599
+ color: #fff;
4600
+ padding: 6px 12px;
4601
+ border-radius: 4px;
4602
+ font-size: 12px;
4603
+ font-weight: 600;
4604
+ white-space: nowrap;
4605
+ pointer-events: none;
4606
+ visibility: hidden;
4607
+ opacity: 0;
4608
+ z-index: 100001;
4609
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4610
+ max-width: 250px;
4611
+ overflow: hidden;
4612
+ text-overflow: ellipsis;
4613
+ transform: translateX(-50%);
4614
+ transition: opacity 0.15s, visibility 0.15s;
4615
+ `;
4616
+ progressContainer.appendChild(chapterTooltip);
4617
+
4618
+ // Get player container bounds for edge detection
4619
+ const getPlayerBounds = () => {
4620
+ return this.api.container.getBoundingClientRect();
4621
+ };
4622
+
4623
+ // Show/hide chapter tooltip with edge detection
4624
+ progressContainer.addEventListener('mousemove', (e) => {
4625
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
4626
+
4627
+ const rect = progressContainer.getBoundingClientRect();
4628
+ const playerRect = getPlayerBounds();
4629
+ const mouseX = e.clientX - rect.left;
4630
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
4631
+ const duration = this.ytPlayer.getDuration();
4632
+ const time = percentage * duration;
4633
+
4634
+ // Find current chapter
4635
+ let currentChapter = null;
4636
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4637
+ if (time >= this.chapters[i].time) {
4638
+ currentChapter = this.chapters[i];
4639
+ break;
4640
+ }
4641
+ }
4642
+
4643
+ if (currentChapter) {
4644
+ chapterTooltip.textContent = currentChapter.title;
4645
+ chapterTooltip.style.visibility = 'visible';
4646
+ chapterTooltip.style.opacity = '1';
4647
+
4648
+ // Wait for tooltip to render to get its width
4649
+ setTimeout(() => {
4650
+ const tooltipWidth = chapterTooltip.offsetWidth;
4651
+ const tooltipHalfWidth = tooltipWidth / 2;
4652
+
4653
+ // Calculate absolute position
4654
+ const absoluteX = e.clientX;
4655
+
4656
+ // Check left edge
4657
+ if (absoluteX - tooltipHalfWidth < playerRect.left) {
4658
+ // Stick to left edge
4659
+ const leftOffset = playerRect.left - rect.left;
4660
+ chapterTooltip.style.left = `${leftOffset + tooltipHalfWidth}px`;
4661
+ chapterTooltip.style.transform = 'translateX(-50%)';
4662
+ }
4663
+ // Check right edge
4664
+ else if (absoluteX + tooltipHalfWidth > playerRect.right) {
4665
+ // Stick to right edge
4666
+ const rightOffset = playerRect.right - rect.left;
4667
+ chapterTooltip.style.left = `${rightOffset - tooltipHalfWidth}px`;
4668
+ chapterTooltip.style.transform = 'translateX(-50%)';
4669
+ }
4670
+ // Normal centering
4671
+ else {
4672
+ chapterTooltip.style.left = `${mouseX}px`;
4673
+ chapterTooltip.style.transform = 'translateX(-50%)';
4674
+ }
4675
+ }, 0);
4676
+ } else {
4677
+ chapterTooltip.style.visibility = 'hidden';
4678
+ chapterTooltip.style.opacity = '0';
4679
+ }
4680
+ });
4681
+
4682
+ progressContainer.addEventListener('mouseleave', () => {
4683
+ chapterTooltip.style.visibility = 'hidden';
4684
+ chapterTooltip.style.opacity = '0';
4685
+ });
4686
+
4687
+ if (this.api.player.options.debug) {
4688
+ console.log('YT Plugin: Chapter tooltip added with edge detection');
4689
+ }
4690
+ }
4691
+
4692
+ /**
4693
+ * Initialize chapter display in title overlay
4694
+ */
4695
+ initChapterDisplayInOverlay() {
4696
+ if (!this.chapters || this.chapters.length === 0) return;
4697
+
4698
+ const titleOverlay = this.api.container.querySelector('.title-overlay');
4699
+ if (!titleOverlay) return;
4700
+
4701
+ // Remove existing chapter element if present
4702
+ let chapterElement = titleOverlay.querySelector('.chapter-name');
4703
+ if (chapterElement) {
4704
+ chapterElement.remove();
4705
+ }
4706
+
4707
+ // Create chapter name element
4708
+ chapterElement = document.createElement('div');
4709
+ chapterElement.className = 'chapter-name';
4710
+ chapterElement.style.cssText = `
4711
+ font-size: 13px;
4712
+ font-weight: 500;
4713
+ color: rgba(255, 255, 255, 0.9);
4714
+ margin-top: 6px;
4715
+ max-width: 400px;
4716
+ overflow: hidden;
4717
+ text-overflow: ellipsis;
4718
+ white-space: nowrap;
4719
+ `;
4720
+
4721
+ // Append to title overlay
4722
+ titleOverlay.appendChild(chapterElement);
4723
+
4724
+ // Start monitoring playback to update chapter name
4725
+ this.startChapterNameMonitoring();
4726
+
4727
+ if (this.api.player.options.debug) {
4728
+ console.log('YT Plugin: Chapter name display initialized in title overlay');
4729
+ }
4730
+ }
4731
+
4732
+ /**
4733
+ * Monitor playback and update chapter name dynamically
4734
+ */
4735
+ startChapterNameMonitoring() {
4736
+ if (this.chapterMonitorInterval) {
4737
+ clearInterval(this.chapterMonitorInterval);
4738
+ }
4739
+
4740
+ // Update every 500ms to catch chapter changes
4741
+ this.chapterMonitorInterval = setInterval(() => {
4742
+ if (!this.ytPlayer || !this.chapters || this.chapters.length === 0) {
4743
+ return;
4744
+ }
4745
+
4746
+ try {
4747
+ const currentTime = this.ytPlayer.getCurrentTime();
4748
+ this.updateChapterNameDisplay(currentTime);
4749
+ } catch (error) {
4750
+ if (this.api.player.options.debug) {
4751
+ console.error('YT Plugin: Error updating chapter name', error);
4752
+ }
4753
+ }
4754
+ }, 500);
4755
+ }
4756
+
4757
+ /**
4758
+ * Update chapter name in overlay based on current time
4759
+ */
4760
+ updateChapterNameDisplay(currentTime) {
4761
+ const chapterElement = this.api.container.querySelector('.title-overlay .chapter-name');
4762
+ if (!chapterElement) return;
4763
+
4764
+ // Find current chapter
4765
+ let currentChapter = null;
4766
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4767
+ if (currentTime >= this.chapters[i].time) {
4768
+ currentChapter = this.chapters[i];
4769
+ break;
4770
+ }
4771
+ }
4772
+
4773
+ // Update or clear chapter name
4774
+ if (currentChapter) {
4775
+ chapterElement.textContent = currentChapter.title;
4776
+ chapterElement.style.display = 'block';
4777
+ } else {
4778
+ chapterElement.style.display = 'none';
4779
+ }
4780
+ }
4781
+
4782
+ /**
4783
+ * Stop chapter name monitoring
4784
+ */
4785
+ stopChapterNameMonitoring() {
4786
+ if (this.chapterMonitorInterval) {
4787
+ clearInterval(this.chapterMonitorInterval);
4788
+ this.chapterMonitorInterval = null;
4789
+ }
4790
+ }
4791
+
4792
+ /**
4793
+ * Enhance progress bar tooltip to show chapter title
4794
+ */
4795
+ enhanceProgressTooltipWithChapters() {
4796
+ if (!this.chapters || this.chapters.length === 0) return;
4797
+
4798
+ const progressContainer = this.api.container.querySelector('.progress-container');
4799
+ if (!progressContainer) return;
4800
+
4801
+ // Find existing tooltip
4802
+ let tooltip = progressContainer.querySelector('.yt-seek-tooltip');
4803
+
4804
+ if (!tooltip) {
4805
+ // Create tooltip if it doesn't exist
4806
+ tooltip = document.createElement('div');
4807
+ tooltip.className = 'yt-seek-tooltip';
4808
+ tooltip.style.cssText = `
4809
+ position: absolute;
4810
+ bottom: calc(100% + 10px);
4811
+ left: 0;
4812
+ background: rgba(28, 28, 28, 0.95);
4813
+ color: #fff;
4814
+ padding: 8px 12px;
4815
+ border-radius: 4px;
4816
+ font-size: 13px;
4817
+ font-weight: 500;
4818
+ white-space: nowrap;
4819
+ pointer-events: none;
4820
+ visibility: hidden;
4821
+ z-index: 99999;
4822
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4823
+ display: flex;
4824
+ flex-direction: column;
4825
+ gap: 4px;
4826
+ min-width: 80px;
4827
+ `;
4828
+ progressContainer.appendChild(tooltip);
4829
+ }
4830
+
4831
+ // Add chapter title element to tooltip
4832
+ let chapterTitle = tooltip.querySelector('.chapter-title');
4833
+ if (!chapterTitle) {
4834
+ chapterTitle = document.createElement('div');
4835
+ chapterTitle.className = 'chapter-title';
4836
+ chapterTitle.style.cssText = `
4837
+ font-size: 12px;
4838
+ font-weight: 600;
4839
+ color: #fff;
4840
+ max-width: 200px;
4841
+ overflow: hidden;
4842
+ text-overflow: ellipsis;
4843
+ white-space: nowrap;
4844
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
4845
+ padding-bottom: 4px;
4846
+ margin-bottom: 2px;
4847
+ `;
4848
+ tooltip.insertBefore(chapterTitle, tooltip.firstChild);
4849
+ }
4850
+
4851
+ // Time display element
4852
+ let timeDisplay = tooltip.querySelector('.time-display');
4853
+ if (!timeDisplay) {
4854
+ timeDisplay = document.createElement('div');
4855
+ timeDisplay.className = 'time-display';
4856
+ timeDisplay.style.cssText = `
4857
+ font-size: 13px;
4858
+ font-weight: 500;
4859
+ color: rgba(255, 255, 255, 0.9);
4860
+ `;
4861
+ tooltip.appendChild(timeDisplay);
4862
+ }
4863
+
4864
+ // Update tooltip on mousemove
4865
+ progressContainer.addEventListener('mousemove', (e) => {
4866
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
4867
+
4868
+ const rect = progressContainer.getBoundingClientRect();
4869
+ const mouseX = e.clientX - rect.left;
4870
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
4871
+ const duration = this.ytPlayer.getDuration();
4872
+ const time = percentage * duration;
4873
+
4874
+ // Find current chapter at this time
4875
+ let currentChapter = null;
4876
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4877
+ if (time >= this.chapters[i].time) {
4878
+ currentChapter = this.chapters[i];
4879
+ break;
4880
+ }
4881
+ }
4882
+
4883
+ // Update chapter title
4884
+ if (currentChapter) {
4885
+ chapterTitle.textContent = currentChapter.title;
4886
+ chapterTitle.style.display = 'block';
4887
+ } else {
4888
+ chapterTitle.style.display = 'none';
4889
+ }
4890
+
4891
+ // Update time display
4892
+ timeDisplay.textContent = this.formatTime(time);
4893
+
4894
+ // Position tooltip
4895
+ tooltip.style.left = `${mouseX}px`;
4896
+ tooltip.style.visibility = 'visible';
4897
+ tooltip.style.transform = 'translateX(-50%)';
4898
+ });
4899
+
4900
+ progressContainer.addEventListener('mouseleave', () => {
4901
+ tooltip.style.visibility = 'hidden';
4902
+ });
4903
+
4904
+ if (this.api.player.options.debug) {
4905
+ console.log('YT Plugin: Progress tooltip enhanced with chapter info');
4906
+ }
4907
+ }
4908
+
4909
+ /**
4910
+ * Clear all cached chapters from localStorage
4911
+ */
4912
+ clearChaptersCache() {
4913
+ try {
4914
+ const keys = [];
4915
+ for (let i = 0; i < localStorage.length; i++) {
4916
+ const key = localStorage.key(i);
4917
+ if (key && key.startsWith(this.cachePrefix)) {
4918
+ keys.push(key);
4919
+ }
4920
+ }
4921
+
4922
+ keys.forEach(key => localStorage.removeItem(key));
4923
+
4924
+ if (this.api.player.options.debug) {
4925
+ console.log(`YT Plugin: Cleared ${keys.length} cached chapters`);
4926
+ }
4927
+ } catch (error) {
4928
+ if (this.api.player.options.debug) {
4929
+ console.error('YT Plugin: Error clearing cache', error);
4930
+ }
4931
+ }
4932
+ }
4933
+
4934
+ /**
4935
+ * Clear expired cache entries from localStorage
4936
+ */
4937
+ clearExpiredCache() {
4938
+ try {
4939
+ const now = Date.now();
4940
+ const keys = [];
4941
+
4942
+ for (let i = 0; i < localStorage.length; i++) {
4943
+ const key = localStorage.key(i);
4944
+ if (key && key.startsWith(this.cachePrefix)) {
4945
+ try {
4946
+ const data = JSON.parse(localStorage.getItem(key));
4947
+ if (now - data.timestamp >= this.cacheExpiration) {
4948
+ keys.push(key);
4949
+ }
4950
+ } catch (e) {
4951
+ // Invalid data, remove it
4952
+ keys.push(key);
4953
+ }
4954
+ }
4955
+ }
4956
+
4957
+ keys.forEach(key => localStorage.removeItem(key));
4958
+
4959
+ if (this.api.player.options.debug) {
4960
+ console.log(`YT Plugin: Cleared ${keys.length} expired cache entries`);
4961
+ }
4962
+ } catch (error) {
4963
+ if (this.api.player.options.debug) {
4964
+ console.error('YT Plugin: Error clearing expired cache', error);
4965
+ }
4966
+ }
4967
+ }
4968
+
4208
4969
  // ===== CLEANUP =====
4209
4970
 
4210
4971
  dispose() {
@@ -4269,9 +5030,8 @@
4269
5030
  const styleEl = document.getElementById('youtube-controls-override');
4270
5031
  if (styleEl) styleEl.remove();
4271
5032
 
4272
- if (this.player.qualities && this.player.qualities.length > 0) {
4273
- // Remove YouTube fake qualities
4274
- }
5033
+ // Stop chapter monitoring when plugin is destroyed
5034
+ this.stopChapterNameMonitoring();
4275
5035
 
4276
5036
  if (this.api.video) {
4277
5037
  this.api.video.style.display = '';