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.4
  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. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js
  15. // @connect get-info.downsub.com
  16. // @connect download.subtitle.to
  17. // @run-at document-idle
  18. // ==/UserScript==
  19.  
  20. (function() {
  21. 'use strict';
  22. const SECRET_KEY = "zthxw34cdp6wfyxmpad38v52t3hsz6c5";
  23. const API = "https://get-info.downsub.com/";
  24. const CryptoJS = window.CryptoJS;
  25. const GM_download = window.GM_download;
  26. const GM_xmlhttpRequest = window.GM_xmlhttpRequest;
  27.  
  28. const formatJson = {
  29. stringify: function (crp) {
  30. let result = {
  31. ct: crp.ciphertext.toString(CryptoJS.enc.Base64)
  32. };
  33. if (crp.iv) {
  34. result.iv = crp.iv.toString();
  35. }
  36. if (crp.salt) {
  37. result.s = crp.salt.toString();
  38. }
  39. return JSON.stringify(result);
  40. },
  41. parse: function (output) {
  42. let parse = JSON.parse(output);
  43. let result = CryptoJS.lib.CipherParams.create({
  44. ciphertext: CryptoJS.enc.Base64.parse(parse.ct)
  45. });
  46. if (parse.iv) {
  47. result.iv = CryptoJS.enc.Hex.parse(parse.iv);
  48. }
  49. if (parse.s) {
  50. result.salt = CryptoJS.enc.Hex.parse(parse.s);
  51. }
  52. return result;
  53. }
  54. };
  55. function _toBase64(payload) {
  56. let vBtoa = btoa(payload);
  57. vBtoa = vBtoa.replace("+", "-");
  58. vBtoa = vBtoa.replace("/", "_");
  59. vBtoa = vBtoa.replace("=", "");
  60. return vBtoa;
  61. }
  62. function _toBinary(base64) {
  63. let data = base64.replace("-", "+");
  64. data = data.replace("_", "/");
  65. const mod4 = data.length % 4;
  66. if (mod4) {
  67. data += "====".substring(mod4);
  68. }
  69. return atob(data);
  70. }
  71. function _encode(payload, options) {
  72. if (!payload) {
  73. return false;
  74. }
  75. let result = CryptoJS.AES.encrypt(JSON.stringify(payload), options || SECRET_KEY, {
  76. format: formatJson
  77. }).toString();
  78. return _toBase64(result).trim();
  79. }
  80. function _decode(payload, options) {
  81. if (!payload) {
  82. return false;
  83. }
  84. let result = CryptoJS.AES.decrypt(_toBinary(payload), options || SECRET_KEY, {
  85. format: formatJson
  86. }).toString(CryptoJS.enc.Utf8);
  87. return result.trim();
  88. }
  89. function _generateData(videoId) {
  90. const url = `https://www.youtube.com/watch?v=${videoId}`;
  91. let id = videoId;
  92. return {
  93. state: 99,
  94. url: url,
  95. urlEncrypt: _encode(url),
  96. source: 0,
  97. id: _encode(id),
  98. playlistId: null
  99. };
  100. }
  101. function _decodeArray(result) {
  102. let subtitles = [], subtitlesAutoTrans = [];
  103. if (result?.subtitles && result?.subtitles.length) {
  104. result.subtitles.forEach((v, i) => {
  105. let ff = {...v};
  106. ff.url = _decode(ff.url).replace(/^"|"$/gi, "");
  107. ff.enc_url = result.subtitles[i].url;
  108. ff.download = {};
  109. const params = new URLSearchParams({
  110. title: encodeURIComponent(ff.name),
  111. url: ff.enc_url
  112. });
  113. ff.download.srt = result.urlSubtitle + "?" + params.toString();
  114. const params2 = new URLSearchParams({
  115. title: encodeURIComponent(ff.name),
  116. url: ff.enc_url,
  117. type: "txt"
  118. });
  119. ff.download.txt = result.urlSubtitle + "?" + params2.toString();
  120. const params3 = new URLSearchParams({
  121. title: encodeURIComponent(ff.name),
  122. url: ff.enc_url,
  123. type: "raw"
  124. });
  125. ff.download.raw = result.urlSubtitle + "?" + params3.toString();
  126. subtitles.push(ff);
  127. });
  128. }
  129. if (result?.subtitlesAutoTrans && result?.subtitlesAutoTrans.length) {
  130. result.subtitlesAutoTrans.forEach((v, i) => {
  131. let ff = {...v};
  132. ff.url = _decode(ff.url).replace(/^"|"$/gi, "");
  133. ff.enc_url = result.subtitlesAutoTrans[i].url;
  134. ff.download = {};
  135. const params = new URLSearchParams({
  136. title: encodeURIComponent(ff.name),
  137. url: ff.enc_url
  138. });
  139. ff.download.srt = result.urlSubtitle + "?" + params.toString();
  140. const params2 = new URLSearchParams({
  141. title: encodeURIComponent(ff.name),
  142. url: ff.enc_url,
  143. type: "txt"
  144. });
  145. ff.download.txt = result.urlSubtitle + "?" + params2.toString();
  146. const params3 = new URLSearchParams({
  147. title: encodeURIComponent(ff.name),
  148. url: ff.enc_url,
  149. type: "raw"
  150. });
  151. ff.download.raw = result.urlSubtitle + "?" + params3.toString();
  152. subtitlesAutoTrans.push(ff);
  153. });
  154. }
  155. return Object.assign(result, {subtitles}, {subtitlesAutoTrans});
  156. }
  157.  
  158. function createSVGIcon(className, isHover = false) {
  159. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  160. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  161.  
  162. svg.setAttribute("viewBox", "0 0 576 512");
  163. svg.classList.add(className);
  164.  
  165. path.setAttribute("d", isHover
  166. ? "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"
  167. : "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"
  168. );
  169.  
  170. svg.appendChild(path);
  171. return svg;
  172. }
  173.  
  174. function createSearchIcon() {
  175. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  176. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  177. svg.setAttribute("viewBox", "0 0 24 24");
  178. svg.setAttribute("width", "16");
  179. svg.setAttribute("height", "16");
  180. 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");
  181. svg.appendChild(path);
  182. return svg;
  183. }
  184.  
  185. function createCheckIcon() {
  186. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  187. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  188. svg.setAttribute("viewBox", "0 0 24 24");
  189. svg.classList.add("check-icon");
  190. path.setAttribute("d", "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z");
  191. svg.appendChild(path);
  192. return svg;
  193. }
  194.  
  195. function getVideoId() {
  196. const urlParams = new URLSearchParams(window.location.search);
  197. return urlParams.get('v');
  198. }
  199.  
  200.  
  201. function downloadSubtitle(url, filename, format, buttonElement) {
  202. try {
  203. const buttonHeight = buttonElement.offsetHeight;
  204. const buttonWidth = buttonElement.offsetWidth;
  205. const originalChildren = Array.from(buttonElement.childNodes).map(node => node.cloneNode(true));
  206. while (buttonElement.firstChild) {
  207. buttonElement.removeChild(buttonElement.firstChild);
  208. }
  209. buttonElement.style.height = `${buttonHeight}px`;
  210. buttonElement.style.width = `${buttonWidth}px`;
  211. const spinner = document.createElement('div');
  212. spinner.className = 'button-spinner';
  213. buttonElement.appendChild(spinner);
  214. buttonElement.disabled = true;
  215. GM_download({
  216. url: url,
  217. name: filename,
  218. onload: function() {
  219. while (buttonElement.firstChild) {
  220. buttonElement.removeChild(buttonElement.firstChild);
  221. }
  222. buttonElement.appendChild(createCheckIcon());
  223. buttonElement.classList.add('download-success');
  224. setTimeout(() => {
  225. while (buttonElement.firstChild) {
  226. buttonElement.removeChild(buttonElement.firstChild);
  227. }
  228. originalChildren.forEach(child => {
  229. buttonElement.appendChild(child.cloneNode(true));
  230. });
  231. buttonElement.disabled = false;
  232. buttonElement.classList.remove('download-success');
  233. buttonElement.style.height = '';
  234. buttonElement.style.width = '';
  235. }, 1500);
  236. },
  237. onerror: function(error) {
  238. console.error('Download error:', error);
  239. while (buttonElement.firstChild) {
  240. buttonElement.removeChild(buttonElement.firstChild);
  241. }
  242. originalChildren.forEach(child => {
  243. buttonElement.appendChild(child.cloneNode(true));
  244. });
  245. buttonElement.disabled = false;
  246. buttonElement.style.height = '';
  247. buttonElement.style.width = '';
  248. }
  249. });
  250. } catch (error) {
  251. console.error('Download setup error:', error);
  252. while (buttonElement.firstChild) {
  253. buttonElement.removeChild(buttonElement.firstChild);
  254. }
  255. buttonElement.textContent = format;
  256. buttonElement.disabled = false;
  257. buttonElement.style.height = '';
  258. buttonElement.style.width = '';
  259. }
  260. }
  261.  
  262. function filterSubtitles(subtitles, query) {
  263. if (!query) return subtitles;
  264. const lowerQuery = query.toLowerCase();
  265. return subtitles.filter(sub =>
  266. sub.name.toLowerCase().includes(lowerQuery)
  267. );
  268. }
  269.  
  270. function createSubtitleTable(subtitles, autoTransSubs, videoTitle) {
  271. const container = document.createElement('div');
  272. container.className = 'subtitle-container';
  273.  
  274. const titleDiv = document.createElement('div');
  275. titleDiv.className = 'subtitle-dropdown-title';
  276. titleDiv.textContent = `Download Subtitles (${subtitles.length + autoTransSubs.length})`;
  277. container.appendChild(titleDiv);
  278. const searchContainer = document.createElement('div');
  279. searchContainer.className = 'subtitle-search-container';
  280. const searchInput = document.createElement('input');
  281. searchInput.type = 'text';
  282. searchInput.className = 'subtitle-search-input';
  283. searchInput.placeholder = 'Search languages...';
  284. const searchIcon = document.createElement('div');
  285. searchIcon.className = 'subtitle-search-icon';
  286. searchIcon.appendChild(createSearchIcon());
  287. searchContainer.appendChild(searchIcon);
  288. searchContainer.appendChild(searchInput);
  289. container.appendChild(searchContainer);
  290.  
  291. const tabsDiv = document.createElement('div');
  292. tabsDiv.className = 'subtitle-tabs';
  293.  
  294. const regularTab = document.createElement('div');
  295. regularTab.className = 'subtitle-tab active';
  296. regularTab.textContent = 'Original';
  297. regularTab.dataset.tab = 'regular';
  298.  
  299. const autoTab = document.createElement('div');
  300. autoTab.className = 'subtitle-tab';
  301. autoTab.textContent = 'Auto Translate';
  302. autoTab.dataset.tab = 'auto';
  303.  
  304. tabsDiv.appendChild(regularTab);
  305. tabsDiv.appendChild(autoTab);
  306. container.appendChild(tabsDiv);
  307.  
  308. const itemsPerPage = 30;
  309. const regularContent = createSubtitleContent(subtitles, videoTitle, true, itemsPerPage);
  310. regularContent.className = 'subtitle-content regular-content active';
  311.  
  312. const autoContent = createSubtitleContent(autoTransSubs, videoTitle, false, itemsPerPage);
  313. autoContent.className = 'subtitle-content auto-content';
  314.  
  315. container.appendChild(regularContent);
  316. container.appendChild(autoContent);
  317.  
  318. tabsDiv.addEventListener('click', (e) => {
  319. if (e.target.classList.contains('subtitle-tab')) {
  320. document.querySelectorAll('.subtitle-tab').forEach(tab => tab.classList.remove('active'));
  321. document.querySelectorAll('.subtitle-content').forEach(content => content.classList.remove('active'));
  322.  
  323. e.target.classList.add('active');
  324. const tabType = e.target.dataset.tab;
  325. document.querySelector(`.${tabType}-content`).classList.add('active');
  326. searchInput.value = '';
  327. const activeContent = document.querySelector(`.${tabType}-content`);
  328. const grid = activeContent.querySelector('.subtitle-grid');
  329. if (tabType === 'regular') {
  330. renderPage(1, subtitles, grid, itemsPerPage, videoTitle);
  331. } else {
  332. renderPage(1, autoTransSubs, grid, itemsPerPage, videoTitle);
  333. }
  334. const pagination = activeContent.querySelector('.subtitle-pagination');
  335. updatePagination(
  336. 1,
  337. Math.ceil((tabType === 'regular' ? subtitles : autoTransSubs).length / itemsPerPage),
  338. pagination,
  339. null,
  340. grid,
  341. tabType === 'regular' ? subtitles : autoTransSubs,
  342. itemsPerPage,
  343. videoTitle
  344. );
  345. }
  346. });
  347. searchInput.addEventListener('input', (e) => {
  348. const query = e.target.value.trim();
  349. const activeTab = document.querySelector('.subtitle-tab.active').dataset.tab;
  350. const activeContent = document.querySelector(`.${activeTab}-content`);
  351. const grid = activeContent.querySelector('.subtitle-grid');
  352. const pagination = activeContent.querySelector('.subtitle-pagination');
  353. const sourceSubtitles = activeTab === 'regular' ? subtitles : autoTransSubs;
  354. const filteredSubtitles = filterSubtitles(sourceSubtitles, query);
  355. renderPage(1, filteredSubtitles, grid, itemsPerPage, videoTitle);
  356. updatePagination(
  357. 1,
  358. Math.ceil(filteredSubtitles.length / itemsPerPage),
  359. pagination,
  360. filteredSubtitles,
  361. grid,
  362. sourceSubtitles,
  363. itemsPerPage,
  364. videoTitle
  365. );
  366. grid.dataset.filteredCount = filteredSubtitles.length;
  367. grid.dataset.query = query;
  368. });
  369.  
  370. return container;
  371. }
  372.  
  373. function renderPage(page, subtitlesList, gridElement, itemsPerPage, videoTitle) {
  374. while (gridElement.firstChild) {
  375. gridElement.removeChild(gridElement.firstChild);
  376. }
  377.  
  378. const startIndex = (page - 1) * itemsPerPage;
  379. const endIndex = Math.min(startIndex + itemsPerPage, subtitlesList.length);
  380.  
  381. for (let i = startIndex; i < endIndex; i++) {
  382. const sub = subtitlesList[i];
  383. const item = document.createElement('div');
  384. item.className = 'subtitle-item';
  385.  
  386. const langLabel = document.createElement('div');
  387. langLabel.className = 'subtitle-language';
  388. langLabel.textContent = sub.name;
  389. item.appendChild(langLabel);
  390.  
  391. const btnContainer = document.createElement('div');
  392. btnContainer.className = 'subtitle-format-container';
  393.  
  394. const srtBtn = document.createElement('button');
  395. srtBtn.textContent = 'SRT';
  396. srtBtn.className = 'subtitle-format-btn srt-btn';
  397. srtBtn.addEventListener('click', (e) => {
  398. e.preventDefault();
  399. e.stopPropagation();
  400. downloadSubtitle(sub.download.srt, `${videoTitle} - ${sub.name}.srt`, 'SRT', srtBtn);
  401. });
  402. btnContainer.appendChild(srtBtn);
  403.  
  404. const txtBtn = document.createElement('button');
  405. txtBtn.textContent = 'TXT';
  406. txtBtn.className = 'subtitle-format-btn txt-btn';
  407. txtBtn.addEventListener('click', (e) => {
  408. e.preventDefault();
  409. e.stopPropagation();
  410. downloadSubtitle(sub.download.txt, `${videoTitle} - ${sub.name}.txt`, 'TXT', txtBtn);
  411. });
  412. btnContainer.appendChild(txtBtn);
  413.  
  414. item.appendChild(btnContainer);
  415. gridElement.appendChild(item);
  416. }
  417. }
  418.  
  419. function updatePagination(page, totalPages, paginationElement, filteredSubs, gridElement, sourceSubtitles, itemsPerPage, videoTitle) {
  420. while (paginationElement.firstChild) {
  421. paginationElement.removeChild(paginationElement.firstChild);
  422. }
  423.  
  424. if (totalPages <= 1) return;
  425.  
  426. const prevBtn = document.createElement('button');
  427. prevBtn.textContent = '«';
  428. prevBtn.className = 'pagination-btn';
  429. prevBtn.disabled = page === 1;
  430. prevBtn.addEventListener('click', (e) => {
  431. e.stopPropagation();
  432. if (page > 1) {
  433. const newPage = page - 1;
  434. const query = gridElement.dataset.query;
  435. const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles;
  436. renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle);
  437. updatePagination(
  438. newPage,
  439. totalPages,
  440. paginationElement,
  441. filteredSubs,
  442. gridElement,
  443. sourceSubtitles,
  444. itemsPerPage,
  445. videoTitle
  446. );
  447. }
  448. });
  449. paginationElement.appendChild(prevBtn);
  450.  
  451. const pageIndicator = document.createElement('span');
  452. pageIndicator.className = 'page-indicator';
  453. pageIndicator.textContent = `${page} / ${totalPages}`;
  454. paginationElement.appendChild(pageIndicator);
  455.  
  456. const nextBtn = document.createElement('button');
  457. nextBtn.textContent = '»';
  458. nextBtn.className = 'pagination-btn';
  459. nextBtn.disabled = page === totalPages;
  460. nextBtn.addEventListener('click', (e) => {
  461. e.stopPropagation();
  462. if (page < totalPages) {
  463. const newPage = page + 1;
  464. const query = gridElement.dataset.query;
  465. const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles;
  466. renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle);
  467. updatePagination(
  468. newPage,
  469. totalPages,
  470. paginationElement,
  471. filteredSubs,
  472. gridElement,
  473. sourceSubtitles,
  474. itemsPerPage,
  475. videoTitle
  476. );
  477. }
  478. });
  479. paginationElement.appendChild(nextBtn);
  480. }
  481.  
  482. function createSubtitleContent(subtitles, videoTitle, isOriginal, itemsPerPage) {
  483. const content = document.createElement('div');
  484. let currentPage = 1;
  485.  
  486. const grid = document.createElement('div');
  487. grid.className = 'subtitle-grid';
  488. if (isOriginal && subtitles.length <= 6) {
  489. grid.classList.add('center-grid');
  490. }
  491. grid.dataset.filteredCount = subtitles.length;
  492. grid.dataset.query = '';
  493.  
  494. const pagination = document.createElement('div');
  495. pagination.className = 'subtitle-pagination';
  496.  
  497. renderPage(currentPage, subtitles, grid, itemsPerPage, videoTitle);
  498. updatePagination(
  499. currentPage,
  500. Math.ceil(subtitles.length / itemsPerPage),
  501. pagination,
  502. null,
  503. grid,
  504. subtitles,
  505. itemsPerPage,
  506. videoTitle
  507. );
  508.  
  509. content.appendChild(grid);
  510. content.appendChild(pagination);
  511.  
  512. return content;
  513. }
  514.  
  515. async function handleSubtitleDownload(e) {
  516. e.preventDefault();
  517. const videoId = getVideoId();
  518.  
  519. if (!videoId) {
  520. console.error('Video ID not found');
  521. return;
  522. }
  523.  
  524. const backdrop = document.createElement('div');
  525. backdrop.className = 'subtitle-backdrop';
  526. document.body.appendChild(backdrop);
  527.  
  528. const loader = document.createElement('div');
  529. loader.className = 'subtitle-loader';
  530. backdrop.appendChild(loader);
  531.  
  532. try {
  533. const data = _generateData(videoId);
  534. const headersList = {
  535. "authority": "get-info.downsub.com",
  536. "accept": "application/json, text/plain, */*",
  537. "accept-language": "id-ID,id;q=0.9",
  538. "origin": "https://downsub.com",
  539. "priority": "u=1, i",
  540. "referer": "https://downsub.com/",
  541. "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
  542. "sec-ch-ua-mobile": "?0",
  543. "sec-ch-ua-platform": '"Windows"',
  544. "sec-fetch-dest": "empty",
  545. "sec-fetch-mode": "cors",
  546. "sec-fetch-site": "same-site",
  547. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
  548. };
  549. const response = await new Promise((resolve, reject) => {
  550. GM_xmlhttpRequest({
  551. method: 'GET',
  552. url: API + data.id,
  553. headers: headersList,
  554. responseType: 'json',
  555. onload: function(response) {
  556. if (response.status >= 200 && response.status < 300) {
  557. resolve(response.response);
  558. } else {
  559. reject(new Error(`Request failed with status ${response.status}`));
  560. }
  561. },
  562. onerror: function() {
  563. reject(new Error('Network error'));
  564. }
  565. });
  566. });
  567.  
  568. const processedResponse = _decodeArray(response);
  569.  
  570. const videoTitleElement = document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata');
  571. const videoTitle = videoTitleElement ? videoTitleElement.textContent.trim() : `youtube_video_${videoId}`;
  572.  
  573. loader.remove();
  574.  
  575. if (!processedResponse.subtitles || processedResponse.subtitles.length === 0 &&
  576. (!processedResponse.subtitlesAutoTrans || processedResponse.subtitlesAutoTrans.length === 0)) {
  577. while (backdrop.firstChild) {
  578. backdrop.removeChild(backdrop.firstChild);
  579. }
  580. const errorDiv = document.createElement('div');
  581. errorDiv.className = 'subtitle-error';
  582. errorDiv.textContent = 'No subtitles available for this video';
  583. backdrop.appendChild(errorDiv);
  584.  
  585. setTimeout(() => {
  586. backdrop.remove();
  587. }, 2000);
  588. return;
  589. }
  590.  
  591. const subtitleTable = createSubtitleTable(
  592. processedResponse.subtitles || [],
  593. processedResponse.subtitlesAutoTrans || [],
  594. videoTitle
  595. );
  596. backdrop.appendChild(subtitleTable);
  597.  
  598. backdrop.addEventListener('click', (e) => {
  599. if (!subtitleTable.contains(e.target)) {
  600. subtitleTable.remove();
  601. backdrop.remove();
  602. }
  603. });
  604.  
  605. subtitleTable.addEventListener('click', (e) => {
  606. e.stopPropagation();
  607. });
  608.  
  609. } catch (error) {
  610. console.error('Error fetching subtitles:', error);
  611.  
  612. while (backdrop.firstChild) {
  613. backdrop.removeChild(backdrop.firstChild);
  614. }
  615. const errorDiv = document.createElement('div');
  616. errorDiv.className = 'subtitle-error';
  617. errorDiv.textContent = 'Error fetching subtitles. Please try again.';
  618. backdrop.appendChild(errorDiv);
  619.  
  620. setTimeout(() => {
  621. backdrop.remove();
  622. }, 2000);
  623. }
  624. }
  625.  
  626. function initializeStyles(computedStyle) {
  627. if (document.querySelector('#yt-subtitle-downloader-styles')) return;
  628.  
  629. const style = document.createElement('style');
  630. style.id = 'yt-subtitle-downloader-styles';
  631. style.textContent = `
  632. .custom-subtitle-btn {
  633. background: none;
  634. border: none;
  635. cursor: pointer;
  636. padding: 0;
  637. width: ${computedStyle.width};
  638. height: ${computedStyle.height};
  639. display: flex;
  640. align-items: center;
  641. justify-content: center;
  642. position: relative;
  643. }
  644. @-moz-document url-prefix() {
  645. .custom-subtitle-btn {
  646. top: 0;
  647. margin-bottom: 0;
  648. vertical-align: top;
  649. }
  650. }
  651. .custom-subtitle-btn svg {
  652. width: 24px;
  653. height: 24px;
  654. fill: #fff;
  655. position: absolute;
  656. top: 50%;
  657. left: 50%;
  658. transform: translate(-50%, -50%);
  659. opacity: 1;
  660. transition: opacity 0.2s ease-in-out;
  661. }
  662. .custom-subtitle-btn .hover-icon {
  663. opacity: 0;
  664. }
  665. .custom-subtitle-btn:hover .default-icon {
  666. opacity: 0;
  667. }
  668. .custom-subtitle-btn:hover .hover-icon {
  669. opacity: 1;
  670. }
  671. .subtitle-backdrop {
  672. position: fixed;
  673. top: 0;
  674. left: 0;
  675. width: 100%;
  676. height: 100%;
  677. background: rgba(0, 0, 0, 0.7);
  678. z-index: 9998;
  679. display: flex;
  680. align-items: center;
  681. justify-content: center;
  682. backdrop-filter: blur(3px);
  683. }
  684. .subtitle-loader {
  685. width: 40px;
  686. height: 40px;
  687. border: 4px solid rgba(255, 255, 255, 0.3);
  688. border-radius: 50%;
  689. border-top: 4px solid #fff;
  690. animation: spin 1s linear infinite;
  691. }
  692. @keyframes spin {
  693. 0% { transform: rotate(0deg); }
  694. 100% { transform: rotate(360deg); }
  695. }
  696. .subtitle-error {
  697. background: rgba(0, 0, 0, 0.8);
  698. color: #fff;
  699. padding: 16px 24px;
  700. border-radius: 8px;
  701. font-size: 14px;
  702. }
  703. .subtitle-container {
  704. position: relative;
  705. background: rgba(28, 28, 28, 0.95);
  706. border: 1px solid rgba(255, 255, 255, 0.1);
  707. border-radius: 8px;
  708. padding: 16px;
  709. z-index: 9999;
  710. min-width: 700px;
  711. max-width: 90vw;
  712. max-height: 80vh;
  713. overflow-y: auto;
  714. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  715. color: #fff;
  716. font-family: 'Roboto', Arial, sans-serif;
  717. }
  718. .subtitle-dropdown-title {
  719. color: #fff;
  720. font-size: 16px;
  721. font-weight: 500;
  722. margin-bottom: 16px;
  723. text-align: center;
  724. }
  725. .subtitle-search-container {
  726. position: relative;
  727. margin-bottom: 16px;
  728. width: 100%;
  729. max-width: 100%;
  730. }
  731. .subtitle-search-input {
  732. width: 100%;
  733. padding: 8px 12px 8px 36px;
  734. border-radius: 4px;
  735. border: 1px solid rgba(255, 255, 255, 0.2);
  736. background: rgba(255, 255, 255, 0.1);
  737. color: white;
  738. font-size: 14px;
  739. box-sizing: border-box;
  740. }
  741. .subtitle-search-input::placeholder {
  742. color: rgba(255, 255, 255, 0.5);
  743. }
  744. .subtitle-search-input:focus {
  745. outline: none;
  746. border-color: rgba(255, 255, 255, 0.4);
  747. background: rgba(255, 255, 255, 0.15);
  748. }
  749. .subtitle-search-icon {
  750. position: absolute;
  751. left: 10px;
  752. top: 50%;
  753. transform: translateY(-50%);
  754. display: flex;
  755. align-items: center;
  756. justify-content: center;
  757. }
  758. .subtitle-search-icon svg {
  759. fill: rgba(255, 255, 255, 0.5);
  760. }
  761. .subtitle-tabs {
  762. display: flex;
  763. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  764. margin-bottom: 16px;
  765. justify-content: center;
  766. }
  767. .subtitle-tab {
  768. padding: 10px 20px;
  769. cursor: pointer;
  770. opacity: 0.7;
  771. transition: all 0.2s;
  772. border-bottom: 2px solid transparent;
  773. font-size: 15px;
  774. font-weight: 500;
  775. }
  776. .subtitle-tab:hover {
  777. opacity: 1;
  778. }
  779. .subtitle-tab.active {
  780. opacity: 1;
  781. border-bottom: 2px solid #2b7fff;
  782. }
  783. .subtitle-content {
  784. display: none;
  785. }
  786. .subtitle-content.active {
  787. display: block;
  788. }
  789. .subtitle-grid {
  790. display: grid;
  791. grid-template-columns: repeat(3, 1fr);
  792. gap: 10px;
  793. margin-bottom: 16px;
  794. }
  795. .subtitle-grid.center-grid {
  796. justify-content: center;
  797. display: flex;
  798. flex-wrap: wrap;
  799. gap: 16px;
  800. }
  801. .center-grid .subtitle-item {
  802. width: 200px;
  803. }
  804. .subtitle-item {
  805. background: rgba(255, 255, 255, 0.05);
  806. border-radius: 6px;
  807. padding: 10px;
  808. transition: all 0.2s;
  809. }
  810. .subtitle-item:hover {
  811. background: rgba(255, 255, 255, 0.1);
  812. }
  813. .subtitle-language {
  814. font-size: 13px;
  815. font-weight: 500;
  816. margin-bottom: 8px;
  817. white-space: nowrap;
  818. overflow: hidden;
  819. text-overflow: ellipsis;
  820. }
  821. .subtitle-format-container {
  822. display: flex;
  823. gap: 8px;
  824. }
  825. .subtitle-format-btn {
  826. flex: 1;
  827. padding: 6px 0;
  828. border-radius: 4px;
  829. border: none;
  830. font-size: 12px;
  831. font-weight: 500;
  832. cursor: pointer;
  833. transition: all 0.2s;
  834. text-align: center;
  835. position: relative;
  836. height: 28px;
  837. line-height: 16px;
  838. }
  839. .button-spinner {
  840. width: 14px;
  841. height: 14px;
  842. border: 2px solid rgba(255, 255, 255, 0.3);
  843. border-radius: 50%;
  844. border-top: 2px solid #fff;
  845. animation: spin 1s linear infinite;
  846. margin: 0 auto;
  847. }
  848. .check-icon {
  849. width: 14px;
  850. height: 14px;
  851. fill: white;
  852. margin: 0 auto;
  853. }
  854. .download-success {
  855. background-color: #00a63e !important;
  856. }
  857. .srt-btn {
  858. background-color: #2b7fff;
  859. color: white;
  860. }
  861. .srt-btn:hover {
  862. background-color: #50a2ff;
  863. }
  864. .txt-btn {
  865. background-color: #615fff;
  866. color: white;
  867. }
  868. .txt-btn:hover {
  869. background-color: #7c86ff;
  870. }
  871. .subtitle-pagination {
  872. display: flex;
  873. justify-content: center;
  874. align-items: center;
  875. margin-top: 16px;
  876. }
  877. .pagination-btn {
  878. background: rgba(255, 255, 255, 0.1);
  879. border: none;
  880. color: white;
  881. width: 32px;
  882. height: 32px;
  883. border-radius: 16px;
  884. cursor: pointer;
  885. display: flex;
  886. align-items: center;
  887. justify-content: center;
  888. font-size: 18px;
  889. transition: all 0.2s;
  890. }
  891. .pagination-btn:not(:disabled):hover {
  892. background: rgba(255, 255, 255, 0.2);
  893. }
  894. .pagination-btn:disabled {
  895. opacity: 0.3;
  896. cursor: not-allowed;
  897. }
  898. .page-indicator {
  899. margin: 0 16px;
  900. font-size: 14px;
  901. color: rgba(255, 255, 255, 0.7);
  902. }
  903. `;
  904. document.head.appendChild(style);
  905. }
  906.  
  907. function initializeButton() {
  908. if (document.querySelector('.custom-subtitle-btn')) return;
  909.  
  910. const originalButton = document.querySelector('.ytp-subtitles-button');
  911. if (!originalButton) return;
  912.  
  913. const newButton = document.createElement('button');
  914. const computedStyle = window.getComputedStyle(originalButton);
  915.  
  916. Object.assign(newButton, {
  917. className: 'ytp-button custom-subtitle-btn',
  918. title: 'Download Subtitles'
  919. });
  920.  
  921. newButton.setAttribute('aria-pressed', 'false');
  922. initializeStyles(computedStyle);
  923.  
  924. newButton.append(
  925. createSVGIcon('default-icon', false),
  926. createSVGIcon('hover-icon', true)
  927. );
  928.  
  929. newButton.addEventListener('click', (e) => {
  930. const existingDropdown = document.querySelector('.subtitle-container');
  931. existingDropdown ? existingDropdown.remove() : handleSubtitleDownload(e);
  932. });
  933.  
  934. originalButton.insertAdjacentElement('afterend', newButton);
  935. }
  936.  
  937. function initializeObserver() {
  938. const observer = new MutationObserver((mutations) => {
  939. mutations.forEach((mutation) => {
  940. if (mutation.addedNodes.length) {
  941. const isVideoPage = window.location.pathname === '/watch';
  942. if (isVideoPage && !document.querySelector('.custom-subtitle-btn')) {
  943. initializeButton();
  944. }
  945. }
  946. });
  947. });
  948.  
  949. function startObserving() {
  950. const playerContainer = document.getElementById('player-container');
  951. const contentContainer = document.getElementById('content');
  952.  
  953. if (playerContainer) {
  954. observer.observe(playerContainer, {
  955. childList: true,
  956. subtree: true
  957. });
  958. }
  959.  
  960. if (contentContainer) {
  961. observer.observe(contentContainer, {
  962. childList: true,
  963. subtree: true
  964. });
  965. }
  966.  
  967. if (window.location.pathname === '/watch') {
  968. initializeButton();
  969. }
  970. }
  971.  
  972. startObserving();
  973.  
  974. if (!document.getElementById('player-container')) {
  975. const retryInterval = setInterval(() => {
  976. if (document.getElementById('player-container')) {
  977. startObserving();
  978. clearInterval(retryInterval);
  979. }
  980. }, 1000);
  981.  
  982. setTimeout(() => clearInterval(retryInterval), 10000);
  983. }
  984.  
  985. const handleNavigation = () => {
  986. if (window.location.pathname === '/watch') {
  987. initializeButton();
  988. }
  989. };
  990.  
  991. window.addEventListener('yt-navigate-finish', handleNavigation);
  992.  
  993. return () => {
  994. observer.disconnect();
  995. window.removeEventListener('yt-navigate-finish', handleNavigation);
  996. };
  997. }
  998.  
  999. function addSubtitleButton() {
  1000. initializeObserver();
  1001. }
  1002.  
  1003. addSubtitleButton();
  1004. })();