Greasy Fork is available in English.

YouTube Enhancer (Subtitle Downloader)

Download Subtitles in Various Languages.

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Subtitle Downloader)
  3. // @description Download Subtitles in Various Languages.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.2
  6. // @author exyezed
  7. // @namespace https://github.com/exyezed/youtube-enhancer/
  8. // @supportURL https://github.com/exyezed/youtube-enhancer/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @match https://youtube.com/*
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_download
  14. // @connect downsub.vercel.app
  15. // @connect download.subtitle.to
  16. // @run-at document-idle
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. function createSVGIcon(className, isHover = false) {
  23. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  24. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  25.  
  26. svg.setAttribute("viewBox", "0 0 576 512");
  27. svg.classList.add(className);
  28.  
  29. path.setAttribute("d", isHover
  30. ? "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm56 208l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm256 0l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zM120 336l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm160 0l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"
  31. : "M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l448 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16L64 80zM0 96C0 60.7 28.7 32 64 32l448 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM120 240l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm256 0l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zM120 336l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm160 0l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"
  32. );
  33.  
  34. svg.appendChild(path);
  35. return svg;
  36. }
  37.  
  38. function createSearchIcon() {
  39. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  40. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  41. svg.setAttribute("viewBox", "0 0 24 24");
  42. svg.setAttribute("width", "16");
  43. svg.setAttribute("height", "16");
  44. path.setAttribute("d", "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z");
  45. svg.appendChild(path);
  46. return svg;
  47. }
  48.  
  49. function createCheckIcon() {
  50. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  51. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  52. svg.setAttribute("viewBox", "0 0 24 24");
  53. svg.classList.add("check-icon");
  54. path.setAttribute("d", "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z");
  55. svg.appendChild(path);
  56. return svg;
  57. }
  58.  
  59. function getVideoId() {
  60. const urlParams = new URLSearchParams(window.location.search);
  61. return urlParams.get('v');
  62. }
  63.  
  64. function createTableElement(tag, text = null) {
  65. const element = document.createElement(tag);
  66. if (text !== null) {
  67. element.textContent = text;
  68. }
  69. return element;
  70. }
  71.  
  72. function downloadSubtitle(url, filename, format, buttonElement) {
  73. try {
  74. const buttonHeight = buttonElement.offsetHeight;
  75. const buttonWidth = buttonElement.offsetWidth;
  76. const originalChildren = Array.from(buttonElement.childNodes).map(node => node.cloneNode(true));
  77. while (buttonElement.firstChild) {
  78. buttonElement.removeChild(buttonElement.firstChild);
  79. }
  80. buttonElement.style.height = `${buttonHeight}px`;
  81. buttonElement.style.width = `${buttonWidth}px`;
  82. const spinner = document.createElement('div');
  83. spinner.className = 'button-spinner';
  84. buttonElement.appendChild(spinner);
  85. buttonElement.disabled = true;
  86. GM_download({
  87. url: url,
  88. name: filename,
  89. onload: function() {
  90. while (buttonElement.firstChild) {
  91. buttonElement.removeChild(buttonElement.firstChild);
  92. }
  93. buttonElement.appendChild(createCheckIcon());
  94. buttonElement.classList.add('download-success');
  95. setTimeout(() => {
  96. while (buttonElement.firstChild) {
  97. buttonElement.removeChild(buttonElement.firstChild);
  98. }
  99. originalChildren.forEach(child => {
  100. buttonElement.appendChild(child.cloneNode(true));
  101. });
  102. buttonElement.disabled = false;
  103. buttonElement.classList.remove('download-success');
  104. buttonElement.style.height = '';
  105. buttonElement.style.width = '';
  106. }, 1500);
  107. },
  108. onerror: function(error) {
  109. console.error('Download error:', error);
  110. while (buttonElement.firstChild) {
  111. buttonElement.removeChild(buttonElement.firstChild);
  112. }
  113. originalChildren.forEach(child => {
  114. buttonElement.appendChild(child.cloneNode(true));
  115. });
  116. buttonElement.disabled = false;
  117. buttonElement.style.height = '';
  118. buttonElement.style.width = '';
  119. }
  120. });
  121. } catch (error) {
  122. console.error('Download setup error:', error);
  123. while (buttonElement.firstChild) {
  124. buttonElement.removeChild(buttonElement.firstChild);
  125. }
  126. buttonElement.textContent = format;
  127. buttonElement.disabled = false;
  128. buttonElement.style.height = '';
  129. buttonElement.style.width = '';
  130. }
  131. }
  132.  
  133. function filterSubtitles(subtitles, query) {
  134. if (!query) return subtitles;
  135. const lowerQuery = query.toLowerCase();
  136. return subtitles.filter(sub =>
  137. sub.name.toLowerCase().includes(lowerQuery)
  138. );
  139. }
  140.  
  141. function createSubtitleTable(subtitles, autoTransSubs, videoTitle) {
  142. const container = document.createElement('div');
  143. container.className = 'subtitle-container';
  144.  
  145. const titleDiv = document.createElement('div');
  146. titleDiv.className = 'subtitle-dropdown-title';
  147. titleDiv.textContent = `Download Subtitles (${subtitles.length + autoTransSubs.length})`;
  148. container.appendChild(titleDiv);
  149. const searchContainer = document.createElement('div');
  150. searchContainer.className = 'subtitle-search-container';
  151. const searchInput = document.createElement('input');
  152. searchInput.type = 'text';
  153. searchInput.className = 'subtitle-search-input';
  154. searchInput.placeholder = 'Search languages...';
  155. const searchIcon = document.createElement('div');
  156. searchIcon.className = 'subtitle-search-icon';
  157. searchIcon.appendChild(createSearchIcon());
  158. searchContainer.appendChild(searchIcon);
  159. searchContainer.appendChild(searchInput);
  160. container.appendChild(searchContainer);
  161.  
  162. const tabsDiv = document.createElement('div');
  163. tabsDiv.className = 'subtitle-tabs';
  164.  
  165. const regularTab = document.createElement('div');
  166. regularTab.className = 'subtitle-tab active';
  167. regularTab.textContent = 'Original';
  168. regularTab.dataset.tab = 'regular';
  169.  
  170. const autoTab = document.createElement('div');
  171. autoTab.className = 'subtitle-tab';
  172. autoTab.textContent = 'Auto Translate';
  173. autoTab.dataset.tab = 'auto';
  174.  
  175. tabsDiv.appendChild(regularTab);
  176. tabsDiv.appendChild(autoTab);
  177. container.appendChild(tabsDiv);
  178.  
  179. const itemsPerPage = 30;
  180. const regularContent = createSubtitleContent(subtitles, videoTitle, true, itemsPerPage);
  181. regularContent.className = 'subtitle-content regular-content active';
  182.  
  183. const autoContent = createSubtitleContent(autoTransSubs, videoTitle, false, itemsPerPage);
  184. autoContent.className = 'subtitle-content auto-content';
  185.  
  186. container.appendChild(regularContent);
  187. container.appendChild(autoContent);
  188.  
  189. tabsDiv.addEventListener('click', (e) => {
  190. if (e.target.classList.contains('subtitle-tab')) {
  191. document.querySelectorAll('.subtitle-tab').forEach(tab => tab.classList.remove('active'));
  192. document.querySelectorAll('.subtitle-content').forEach(content => content.classList.remove('active'));
  193.  
  194. e.target.classList.add('active');
  195. const tabType = e.target.dataset.tab;
  196. document.querySelector(`.${tabType}-content`).classList.add('active');
  197. searchInput.value = '';
  198. const activeContent = document.querySelector(`.${tabType}-content`);
  199. const grid = activeContent.querySelector('.subtitle-grid');
  200. if (tabType === 'regular') {
  201. renderPage(1, subtitles, grid, itemsPerPage, videoTitle);
  202. } else {
  203. renderPage(1, autoTransSubs, grid, itemsPerPage, videoTitle);
  204. }
  205. const pagination = activeContent.querySelector('.subtitle-pagination');
  206. updatePagination(
  207. 1,
  208. Math.ceil((tabType === 'regular' ? subtitles : autoTransSubs).length / itemsPerPage),
  209. pagination,
  210. null,
  211. grid,
  212. tabType === 'regular' ? subtitles : autoTransSubs,
  213. itemsPerPage,
  214. videoTitle
  215. );
  216. }
  217. });
  218. searchInput.addEventListener('input', (e) => {
  219. const query = e.target.value.trim();
  220. const activeTab = document.querySelector('.subtitle-tab.active').dataset.tab;
  221. const activeContent = document.querySelector(`.${activeTab}-content`);
  222. const grid = activeContent.querySelector('.subtitle-grid');
  223. const pagination = activeContent.querySelector('.subtitle-pagination');
  224. const sourceSubtitles = activeTab === 'regular' ? subtitles : autoTransSubs;
  225. const filteredSubtitles = filterSubtitles(sourceSubtitles, query);
  226. renderPage(1, filteredSubtitles, grid, itemsPerPage, videoTitle);
  227. updatePagination(
  228. 1,
  229. Math.ceil(filteredSubtitles.length / itemsPerPage),
  230. pagination,
  231. filteredSubtitles,
  232. grid,
  233. sourceSubtitles,
  234. itemsPerPage,
  235. videoTitle
  236. );
  237. grid.dataset.filteredCount = filteredSubtitles.length;
  238. grid.dataset.query = query;
  239. });
  240.  
  241. return container;
  242. }
  243.  
  244. function renderPage(page, subtitlesList, gridElement, itemsPerPage, videoTitle) {
  245. while (gridElement.firstChild) {
  246. gridElement.removeChild(gridElement.firstChild);
  247. }
  248.  
  249. const startIndex = (page - 1) * itemsPerPage;
  250. const endIndex = Math.min(startIndex + itemsPerPage, subtitlesList.length);
  251.  
  252. for (let i = startIndex; i < endIndex; i++) {
  253. const sub = subtitlesList[i];
  254. const item = document.createElement('div');
  255. item.className = 'subtitle-item';
  256.  
  257. const langLabel = document.createElement('div');
  258. langLabel.className = 'subtitle-language';
  259. langLabel.textContent = sub.name;
  260. item.appendChild(langLabel);
  261.  
  262. const btnContainer = document.createElement('div');
  263. btnContainer.className = 'subtitle-format-container';
  264.  
  265. const srtBtn = document.createElement('button');
  266. srtBtn.textContent = 'SRT';
  267. srtBtn.className = 'subtitle-format-btn srt-btn';
  268. srtBtn.addEventListener('click', (e) => {
  269. e.preventDefault();
  270. e.stopPropagation();
  271. downloadSubtitle(sub.download.srt, `${videoTitle} - ${sub.name}.srt`, 'SRT', srtBtn);
  272. });
  273. btnContainer.appendChild(srtBtn);
  274.  
  275. const txtBtn = document.createElement('button');
  276. txtBtn.textContent = 'TXT';
  277. txtBtn.className = 'subtitle-format-btn txt-btn';
  278. txtBtn.addEventListener('click', (e) => {
  279. e.preventDefault();
  280. e.stopPropagation();
  281. downloadSubtitle(sub.download.txt, `${videoTitle} - ${sub.name}.txt`, 'TXT', txtBtn);
  282. });
  283. btnContainer.appendChild(txtBtn);
  284.  
  285. item.appendChild(btnContainer);
  286. gridElement.appendChild(item);
  287. }
  288. }
  289.  
  290. function updatePagination(page, totalPages, paginationElement, filteredSubs, gridElement, sourceSubtitles, itemsPerPage, videoTitle) {
  291. while (paginationElement.firstChild) {
  292. paginationElement.removeChild(paginationElement.firstChild);
  293. }
  294.  
  295. if (totalPages <= 1) return;
  296.  
  297. const prevBtn = document.createElement('button');
  298. prevBtn.textContent = '«';
  299. prevBtn.className = 'pagination-btn';
  300. prevBtn.disabled = page === 1;
  301. prevBtn.addEventListener('click', (e) => {
  302. e.stopPropagation();
  303. if (page > 1) {
  304. const newPage = page - 1;
  305. const query = gridElement.dataset.query;
  306. const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles;
  307. renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle);
  308. updatePagination(
  309. newPage,
  310. totalPages,
  311. paginationElement,
  312. filteredSubs,
  313. gridElement,
  314. sourceSubtitles,
  315. itemsPerPage,
  316. videoTitle
  317. );
  318. }
  319. });
  320. paginationElement.appendChild(prevBtn);
  321.  
  322. const pageIndicator = document.createElement('span');
  323. pageIndicator.className = 'page-indicator';
  324. pageIndicator.textContent = `${page} / ${totalPages}`;
  325. paginationElement.appendChild(pageIndicator);
  326.  
  327. const nextBtn = document.createElement('button');
  328. nextBtn.textContent = '»';
  329. nextBtn.className = 'pagination-btn';
  330. nextBtn.disabled = page === totalPages;
  331. nextBtn.addEventListener('click', (e) => {
  332. e.stopPropagation();
  333. if (page < totalPages) {
  334. const newPage = page + 1;
  335. const query = gridElement.dataset.query;
  336. const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles;
  337. renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle);
  338. updatePagination(
  339. newPage,
  340. totalPages,
  341. paginationElement,
  342. filteredSubs,
  343. gridElement,
  344. sourceSubtitles,
  345. itemsPerPage,
  346. videoTitle
  347. );
  348. }
  349. });
  350. paginationElement.appendChild(nextBtn);
  351. }
  352.  
  353. function createSubtitleContent(subtitles, videoTitle, isOriginal, itemsPerPage) {
  354. const content = document.createElement('div');
  355. let currentPage = 1;
  356.  
  357. const grid = document.createElement('div');
  358. grid.className = 'subtitle-grid';
  359. if (isOriginal && subtitles.length <= 6) {
  360. grid.classList.add('center-grid');
  361. }
  362. grid.dataset.filteredCount = subtitles.length;
  363. grid.dataset.query = '';
  364.  
  365. const pagination = document.createElement('div');
  366. pagination.className = 'subtitle-pagination';
  367.  
  368. renderPage(currentPage, subtitles, grid, itemsPerPage, videoTitle);
  369. updatePagination(
  370. currentPage,
  371. Math.ceil(subtitles.length / itemsPerPage),
  372. pagination,
  373. null,
  374. grid,
  375. subtitles,
  376. itemsPerPage,
  377. videoTitle
  378. );
  379.  
  380. content.appendChild(grid);
  381. content.appendChild(pagination);
  382.  
  383. return content;
  384. }
  385.  
  386. async function handleSubtitleDownload(e) {
  387. e.preventDefault();
  388. const videoId = getVideoId();
  389.  
  390. if (!videoId) {
  391. console.error('Video ID not found');
  392. return;
  393. }
  394.  
  395. const backdrop = document.createElement('div');
  396. backdrop.className = 'subtitle-backdrop';
  397. document.body.appendChild(backdrop);
  398.  
  399. const loader = document.createElement('div');
  400. loader.className = 'subtitle-loader';
  401. backdrop.appendChild(loader);
  402.  
  403. try {
  404. const response = await new Promise((resolve, reject) => {
  405. GM_xmlhttpRequest({
  406. method: 'GET',
  407. url: `https://downsub.vercel.app/${videoId}`,
  408. headers: {
  409. 'Accept': 'application/json'
  410. },
  411. responseType: 'json',
  412. onload: function(response) {
  413. if (response.status >= 200 && response.status < 300) {
  414. resolve(response.response);
  415. } else {
  416. reject(new Error(`Request failed with status ${response.status}`));
  417. }
  418. },
  419. onerror: function(error) {
  420. reject(new Error('Network error'));
  421. }
  422. });
  423. });
  424.  
  425. const videoTitleElement = document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata');
  426. const videoTitle = videoTitleElement ? videoTitleElement.textContent.trim() : `youtube_video_${videoId}`;
  427.  
  428. loader.remove();
  429.  
  430. if (!response.subtitles || response.subtitles.length === 0 &&
  431. (!response.subtitlesAutoTrans || response.subtitlesAutoTrans.length === 0)) {
  432. while (backdrop.firstChild) {
  433. backdrop.removeChild(backdrop.firstChild);
  434. }
  435. const errorDiv = document.createElement('div');
  436. errorDiv.className = 'subtitle-error';
  437. errorDiv.textContent = 'No subtitles available for this video';
  438. backdrop.appendChild(errorDiv);
  439.  
  440. setTimeout(() => {
  441. backdrop.remove();
  442. }, 2000);
  443. return;
  444. }
  445.  
  446. const subtitleTable = createSubtitleTable(
  447. response.subtitles || [],
  448. response.subtitlesAutoTrans || [],
  449. videoTitle
  450. );
  451. backdrop.appendChild(subtitleTable);
  452.  
  453. backdrop.addEventListener('click', (e) => {
  454. if (!subtitleTable.contains(e.target)) {
  455. subtitleTable.remove();
  456. backdrop.remove();
  457. }
  458. });
  459.  
  460. subtitleTable.addEventListener('click', (e) => {
  461. e.stopPropagation();
  462. });
  463.  
  464. } catch (error) {
  465. console.error('Error fetching subtitles:', error);
  466.  
  467. while (backdrop.firstChild) {
  468. backdrop.removeChild(backdrop.firstChild);
  469. }
  470. const errorDiv = document.createElement('div');
  471. errorDiv.className = 'subtitle-error';
  472. errorDiv.textContent = 'Error fetching subtitles. Please try again.';
  473. backdrop.appendChild(errorDiv);
  474.  
  475. setTimeout(() => {
  476. backdrop.remove();
  477. }, 2000);
  478. }
  479. }
  480.  
  481. function initializeStyles(computedStyle) {
  482. if (document.querySelector('#yt-subtitle-downloader-styles')) return;
  483.  
  484. const style = document.createElement('style');
  485. style.id = 'yt-subtitle-downloader-styles';
  486. style.textContent = `
  487. .custom-subtitle-btn {
  488. background: none;
  489. border: none;
  490. cursor: pointer;
  491. padding: 0;
  492. width: ${computedStyle.width};
  493. height: ${computedStyle.height};
  494. display: flex;
  495. align-items: center;
  496. justify-content: center;
  497. position: relative;
  498. }
  499. .custom-subtitle-btn svg {
  500. width: 24px;
  501. height: 24px;
  502. fill: #fff;
  503. position: absolute;
  504. top: 50%;
  505. left: 50%;
  506. transform: translate(-50%, -50%);
  507. opacity: 1;
  508. transition: opacity 0.2s ease-in-out;
  509. }
  510. .custom-subtitle-btn .hover-icon {
  511. opacity: 0;
  512. }
  513. .custom-subtitle-btn:hover .default-icon {
  514. opacity: 0;
  515. }
  516. .custom-subtitle-btn:hover .hover-icon {
  517. opacity: 1;
  518. }
  519. .subtitle-backdrop {
  520. position: fixed;
  521. top: 0;
  522. left: 0;
  523. width: 100%;
  524. height: 100%;
  525. background: rgba(0, 0, 0, 0.7);
  526. z-index: 9998;
  527. display: flex;
  528. align-items: center;
  529. justify-content: center;
  530. backdrop-filter: blur(3px);
  531. }
  532. .subtitle-loader {
  533. width: 40px;
  534. height: 40px;
  535. border: 4px solid rgba(255, 255, 255, 0.3);
  536. border-radius: 50%;
  537. border-top: 4px solid #fff;
  538. animation: spin 1s linear infinite;
  539. }
  540. @keyframes spin {
  541. 0% { transform: rotate(0deg); }
  542. 100% { transform: rotate(360deg); }
  543. }
  544. .subtitle-error {
  545. background: rgba(0, 0, 0, 0.8);
  546. color: #fff;
  547. padding: 16px 24px;
  548. border-radius: 8px;
  549. font-size: 14px;
  550. }
  551. .subtitle-container {
  552. position: relative;
  553. background: rgba(28, 28, 28, 0.95);
  554. border: 1px solid rgba(255, 255, 255, 0.1);
  555. border-radius: 8px;
  556. padding: 16px;
  557. z-index: 9999;
  558. min-width: 700px;
  559. max-width: 90vw;
  560. max-height: 80vh;
  561. overflow-y: auto;
  562. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  563. color: #fff;
  564. font-family: 'Roboto', Arial, sans-serif;
  565. }
  566. .subtitle-dropdown-title {
  567. color: #fff;
  568. font-size: 16px;
  569. font-weight: 500;
  570. margin-bottom: 16px;
  571. text-align: center;
  572. }
  573. .subtitle-search-container {
  574. position: relative;
  575. margin-bottom: 16px;
  576. width: 100%;
  577. max-width: 100%;
  578. }
  579. .subtitle-search-input {
  580. width: 100%;
  581. padding: 8px 12px 8px 36px;
  582. border-radius: 4px;
  583. border: 1px solid rgba(255, 255, 255, 0.2);
  584. background: rgba(255, 255, 255, 0.1);
  585. color: white;
  586. font-size: 14px;
  587. box-sizing: border-box;
  588. }
  589. .subtitle-search-input::placeholder {
  590. color: rgba(255, 255, 255, 0.5);
  591. }
  592. .subtitle-search-input:focus {
  593. outline: none;
  594. border-color: rgba(255, 255, 255, 0.4);
  595. background: rgba(255, 255, 255, 0.15);
  596. }
  597. .subtitle-search-icon {
  598. position: absolute;
  599. left: 10px;
  600. top: 50%;
  601. transform: translateY(-50%);
  602. display: flex;
  603. align-items: center;
  604. justify-content: center;
  605. }
  606. .subtitle-search-icon svg {
  607. fill: rgba(255, 255, 255, 0.5);
  608. }
  609. .subtitle-tabs {
  610. display: flex;
  611. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  612. margin-bottom: 16px;
  613. justify-content: center;
  614. }
  615. .subtitle-tab {
  616. padding: 10px 20px;
  617. cursor: pointer;
  618. opacity: 0.7;
  619. transition: all 0.2s;
  620. border-bottom: 2px solid transparent;
  621. font-size: 15px;
  622. font-weight: 500;
  623. }
  624. .subtitle-tab:hover {
  625. opacity: 1;
  626. }
  627. .subtitle-tab.active {
  628. opacity: 1;
  629. border-bottom: 2px solid #2b7fff;
  630. }
  631. .subtitle-content {
  632. display: none;
  633. }
  634. .subtitle-content.active {
  635. display: block;
  636. }
  637. .subtitle-grid {
  638. display: grid;
  639. grid-template-columns: repeat(3, 1fr);
  640. gap: 10px;
  641. margin-bottom: 16px;
  642. }
  643. .subtitle-grid.center-grid {
  644. justify-content: center;
  645. display: flex;
  646. flex-wrap: wrap;
  647. gap: 16px;
  648. }
  649. .center-grid .subtitle-item {
  650. width: 200px;
  651. }
  652. .subtitle-item {
  653. background: rgba(255, 255, 255, 0.05);
  654. border-radius: 6px;
  655. padding: 10px;
  656. transition: all 0.2s;
  657. }
  658. .subtitle-item:hover {
  659. background: rgba(255, 255, 255, 0.1);
  660. }
  661. .subtitle-language {
  662. font-size: 13px;
  663. font-weight: 500;
  664. margin-bottom: 8px;
  665. white-space: nowrap;
  666. overflow: hidden;
  667. text-overflow: ellipsis;
  668. }
  669. .subtitle-format-container {
  670. display: flex;
  671. gap: 8px;
  672. }
  673. .subtitle-format-btn {
  674. flex: 1;
  675. padding: 6px 0;
  676. border-radius: 4px;
  677. border: none;
  678. font-size: 12px;
  679. font-weight: 500;
  680. cursor: pointer;
  681. transition: all 0.2s;
  682. text-align: center;
  683. position: relative;
  684. height: 28px;
  685. line-height: 16px;
  686. }
  687. .button-spinner {
  688. width: 14px;
  689. height: 14px;
  690. border: 2px solid rgba(255, 255, 255, 0.3);
  691. border-radius: 50%;
  692. border-top: 2px solid #fff;
  693. animation: spin 1s linear infinite;
  694. margin: 0 auto;
  695. }
  696. .check-icon {
  697. width: 14px;
  698. height: 14px;
  699. fill: white;
  700. margin: 0 auto;
  701. }
  702. .download-success {
  703. background-color: #00a63e !important;
  704. }
  705. .srt-btn {
  706. background-color: #2b7fff;
  707. color: white;
  708. }
  709. .srt-btn:hover {
  710. background-color: #50a2ff;
  711. }
  712. .txt-btn {
  713. background-color: #615fff;
  714. color: white;
  715. }
  716. .txt-btn:hover {
  717. background-color: #7c86ff;
  718. }
  719. .subtitle-pagination {
  720. display: flex;
  721. justify-content: center;
  722. align-items: center;
  723. margin-top: 16px;
  724. }
  725. .pagination-btn {
  726. background: rgba(255, 255, 255, 0.1);
  727. border: none;
  728. color: white;
  729. width: 32px;
  730. height: 32px;
  731. border-radius: 16px;
  732. cursor: pointer;
  733. display: flex;
  734. align-items: center;
  735. justify-content: center;
  736. font-size: 18px;
  737. transition: all 0.2s;
  738. }
  739. .pagination-btn:not(:disabled):hover {
  740. background: rgba(255, 255, 255, 0.2);
  741. }
  742. .pagination-btn:disabled {
  743. opacity: 0.3;
  744. cursor: not-allowed;
  745. }
  746. .page-indicator {
  747. margin: 0 16px;
  748. font-size: 14px;
  749. color: rgba(255, 255, 255, 0.7);
  750. }
  751. `;
  752. document.head.appendChild(style);
  753. }
  754.  
  755. function initializeButton() {
  756. if (document.querySelector('.custom-subtitle-btn')) return;
  757.  
  758. const originalButton = document.querySelector('.ytp-subtitles-button');
  759. if (!originalButton) return;
  760.  
  761. const newButton = document.createElement('button');
  762. const computedStyle = window.getComputedStyle(originalButton);
  763.  
  764. Object.assign(newButton, {
  765. className: 'ytp-button custom-subtitle-btn',
  766. title: 'Download Subtitles'
  767. });
  768.  
  769. newButton.setAttribute('aria-pressed', 'false');
  770. initializeStyles(computedStyle);
  771.  
  772. newButton.append(
  773. createSVGIcon('default-icon', false),
  774. createSVGIcon('hover-icon', true)
  775. );
  776.  
  777. newButton.addEventListener('click', (e) => {
  778. const existingDropdown = document.querySelector('.subtitle-container');
  779. existingDropdown ? existingDropdown.remove() : handleSubtitleDownload(e);
  780. });
  781.  
  782. originalButton.insertAdjacentElement('afterend', newButton);
  783. }
  784.  
  785. function initializeObserver() {
  786. const observer = new MutationObserver((mutations) => {
  787. mutations.forEach((mutation) => {
  788. if (mutation.addedNodes.length) {
  789. const isVideoPage = window.location.pathname === '/watch';
  790. if (isVideoPage && !document.querySelector('.custom-subtitle-btn')) {
  791. initializeButton();
  792. }
  793. }
  794. });
  795. });
  796.  
  797. function startObserving() {
  798. const playerContainer = document.getElementById('player-container');
  799. const contentContainer = document.getElementById('content');
  800.  
  801. if (playerContainer) {
  802. observer.observe(playerContainer, {
  803. childList: true,
  804. subtree: true
  805. });
  806. }
  807.  
  808. if (contentContainer) {
  809. observer.observe(contentContainer, {
  810. childList: true,
  811. subtree: true
  812. });
  813. }
  814.  
  815. if (window.location.pathname === '/watch') {
  816. initializeButton();
  817. }
  818. }
  819.  
  820. startObserving();
  821.  
  822. if (!document.getElementById('player-container')) {
  823. const retryInterval = setInterval(() => {
  824. if (document.getElementById('player-container')) {
  825. startObserving();
  826. clearInterval(retryInterval);
  827. }
  828. }, 1000);
  829.  
  830. setTimeout(() => clearInterval(retryInterval), 10000);
  831. }
  832.  
  833. const handleNavigation = () => {
  834. if (window.location.pathname === '/watch') {
  835. initializeButton();
  836. }
  837. };
  838.  
  839. window.addEventListener('yt-navigate-finish', handleNavigation);
  840.  
  841. return () => {
  842. observer.disconnect();
  843. window.removeEventListener('yt-navigate-finish', handleNavigation);
  844. };
  845. }
  846.  
  847. function addSubtitleButton() {
  848. initializeObserver();
  849. }
  850.  
  851. addSubtitleButton();
  852. })();