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.3
  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. .custom-subtitle-btn svg {
  645. width: 24px;
  646. height: 24px;
  647. fill: #fff;
  648. position: absolute;
  649. top: 50%;
  650. left: 50%;
  651. transform: translate(-50%, -50%);
  652. opacity: 1;
  653. transition: opacity 0.2s ease-in-out;
  654. }
  655. .custom-subtitle-btn .hover-icon {
  656. opacity: 0;
  657. }
  658. .custom-subtitle-btn:hover .default-icon {
  659. opacity: 0;
  660. }
  661. .custom-subtitle-btn:hover .hover-icon {
  662. opacity: 1;
  663. }
  664. .subtitle-backdrop {
  665. position: fixed;
  666. top: 0;
  667. left: 0;
  668. width: 100%;
  669. height: 100%;
  670. background: rgba(0, 0, 0, 0.7);
  671. z-index: 9998;
  672. display: flex;
  673. align-items: center;
  674. justify-content: center;
  675. backdrop-filter: blur(3px);
  676. }
  677. .subtitle-loader {
  678. width: 40px;
  679. height: 40px;
  680. border: 4px solid rgba(255, 255, 255, 0.3);
  681. border-radius: 50%;
  682. border-top: 4px solid #fff;
  683. animation: spin 1s linear infinite;
  684. }
  685. @keyframes spin {
  686. 0% { transform: rotate(0deg); }
  687. 100% { transform: rotate(360deg); }
  688. }
  689. .subtitle-error {
  690. background: rgba(0, 0, 0, 0.8);
  691. color: #fff;
  692. padding: 16px 24px;
  693. border-radius: 8px;
  694. font-size: 14px;
  695. }
  696. .subtitle-container {
  697. position: relative;
  698. background: rgba(28, 28, 28, 0.95);
  699. border: 1px solid rgba(255, 255, 255, 0.1);
  700. border-radius: 8px;
  701. padding: 16px;
  702. z-index: 9999;
  703. min-width: 700px;
  704. max-width: 90vw;
  705. max-height: 80vh;
  706. overflow-y: auto;
  707. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  708. color: #fff;
  709. font-family: 'Roboto', Arial, sans-serif;
  710. }
  711. .subtitle-dropdown-title {
  712. color: #fff;
  713. font-size: 16px;
  714. font-weight: 500;
  715. margin-bottom: 16px;
  716. text-align: center;
  717. }
  718. .subtitle-search-container {
  719. position: relative;
  720. margin-bottom: 16px;
  721. width: 100%;
  722. max-width: 100%;
  723. }
  724. .subtitle-search-input {
  725. width: 100%;
  726. padding: 8px 12px 8px 36px;
  727. border-radius: 4px;
  728. border: 1px solid rgba(255, 255, 255, 0.2);
  729. background: rgba(255, 255, 255, 0.1);
  730. color: white;
  731. font-size: 14px;
  732. box-sizing: border-box;
  733. }
  734. .subtitle-search-input::placeholder {
  735. color: rgba(255, 255, 255, 0.5);
  736. }
  737. .subtitle-search-input:focus {
  738. outline: none;
  739. border-color: rgba(255, 255, 255, 0.4);
  740. background: rgba(255, 255, 255, 0.15);
  741. }
  742. .subtitle-search-icon {
  743. position: absolute;
  744. left: 10px;
  745. top: 50%;
  746. transform: translateY(-50%);
  747. display: flex;
  748. align-items: center;
  749. justify-content: center;
  750. }
  751. .subtitle-search-icon svg {
  752. fill: rgba(255, 255, 255, 0.5);
  753. }
  754. .subtitle-tabs {
  755. display: flex;
  756. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  757. margin-bottom: 16px;
  758. justify-content: center;
  759. }
  760. .subtitle-tab {
  761. padding: 10px 20px;
  762. cursor: pointer;
  763. opacity: 0.7;
  764. transition: all 0.2s;
  765. border-bottom: 2px solid transparent;
  766. font-size: 15px;
  767. font-weight: 500;
  768. }
  769. .subtitle-tab:hover {
  770. opacity: 1;
  771. }
  772. .subtitle-tab.active {
  773. opacity: 1;
  774. border-bottom: 2px solid #2b7fff;
  775. }
  776. .subtitle-content {
  777. display: none;
  778. }
  779. .subtitle-content.active {
  780. display: block;
  781. }
  782. .subtitle-grid {
  783. display: grid;
  784. grid-template-columns: repeat(3, 1fr);
  785. gap: 10px;
  786. margin-bottom: 16px;
  787. }
  788. .subtitle-grid.center-grid {
  789. justify-content: center;
  790. display: flex;
  791. flex-wrap: wrap;
  792. gap: 16px;
  793. }
  794. .center-grid .subtitle-item {
  795. width: 200px;
  796. }
  797. .subtitle-item {
  798. background: rgba(255, 255, 255, 0.05);
  799. border-radius: 6px;
  800. padding: 10px;
  801. transition: all 0.2s;
  802. }
  803. .subtitle-item:hover {
  804. background: rgba(255, 255, 255, 0.1);
  805. }
  806. .subtitle-language {
  807. font-size: 13px;
  808. font-weight: 500;
  809. margin-bottom: 8px;
  810. white-space: nowrap;
  811. overflow: hidden;
  812. text-overflow: ellipsis;
  813. }
  814. .subtitle-format-container {
  815. display: flex;
  816. gap: 8px;
  817. }
  818. .subtitle-format-btn {
  819. flex: 1;
  820. padding: 6px 0;
  821. border-radius: 4px;
  822. border: none;
  823. font-size: 12px;
  824. font-weight: 500;
  825. cursor: pointer;
  826. transition: all 0.2s;
  827. text-align: center;
  828. position: relative;
  829. height: 28px;
  830. line-height: 16px;
  831. }
  832. .button-spinner {
  833. width: 14px;
  834. height: 14px;
  835. border: 2px solid rgba(255, 255, 255, 0.3);
  836. border-radius: 50%;
  837. border-top: 2px solid #fff;
  838. animation: spin 1s linear infinite;
  839. margin: 0 auto;
  840. }
  841. .check-icon {
  842. width: 14px;
  843. height: 14px;
  844. fill: white;
  845. margin: 0 auto;
  846. }
  847. .download-success {
  848. background-color: #00a63e !important;
  849. }
  850. .srt-btn {
  851. background-color: #2b7fff;
  852. color: white;
  853. }
  854. .srt-btn:hover {
  855. background-color: #50a2ff;
  856. }
  857. .txt-btn {
  858. background-color: #615fff;
  859. color: white;
  860. }
  861. .txt-btn:hover {
  862. background-color: #7c86ff;
  863. }
  864. .subtitle-pagination {
  865. display: flex;
  866. justify-content: center;
  867. align-items: center;
  868. margin-top: 16px;
  869. }
  870. .pagination-btn {
  871. background: rgba(255, 255, 255, 0.1);
  872. border: none;
  873. color: white;
  874. width: 32px;
  875. height: 32px;
  876. border-radius: 16px;
  877. cursor: pointer;
  878. display: flex;
  879. align-items: center;
  880. justify-content: center;
  881. font-size: 18px;
  882. transition: all 0.2s;
  883. }
  884. .pagination-btn:not(:disabled):hover {
  885. background: rgba(255, 255, 255, 0.2);
  886. }
  887. .pagination-btn:disabled {
  888. opacity: 0.3;
  889. cursor: not-allowed;
  890. }
  891. .page-indicator {
  892. margin: 0 16px;
  893. font-size: 14px;
  894. color: rgba(255, 255, 255, 0.7);
  895. }
  896. `;
  897. document.head.appendChild(style);
  898. }
  899.  
  900. function initializeButton() {
  901. if (document.querySelector('.custom-subtitle-btn')) return;
  902.  
  903. const originalButton = document.querySelector('.ytp-subtitles-button');
  904. if (!originalButton) return;
  905.  
  906. const newButton = document.createElement('button');
  907. const computedStyle = window.getComputedStyle(originalButton);
  908.  
  909. Object.assign(newButton, {
  910. className: 'ytp-button custom-subtitle-btn',
  911. title: 'Download Subtitles'
  912. });
  913.  
  914. newButton.setAttribute('aria-pressed', 'false');
  915. initializeStyles(computedStyle);
  916.  
  917. newButton.append(
  918. createSVGIcon('default-icon', false),
  919. createSVGIcon('hover-icon', true)
  920. );
  921.  
  922. newButton.addEventListener('click', (e) => {
  923. const existingDropdown = document.querySelector('.subtitle-container');
  924. existingDropdown ? existingDropdown.remove() : handleSubtitleDownload(e);
  925. });
  926.  
  927. originalButton.insertAdjacentElement('afterend', newButton);
  928. }
  929.  
  930. function initializeObserver() {
  931. const observer = new MutationObserver((mutations) => {
  932. mutations.forEach((mutation) => {
  933. if (mutation.addedNodes.length) {
  934. const isVideoPage = window.location.pathname === '/watch';
  935. if (isVideoPage && !document.querySelector('.custom-subtitle-btn')) {
  936. initializeButton();
  937. }
  938. }
  939. });
  940. });
  941.  
  942. function startObserving() {
  943. const playerContainer = document.getElementById('player-container');
  944. const contentContainer = document.getElementById('content');
  945.  
  946. if (playerContainer) {
  947. observer.observe(playerContainer, {
  948. childList: true,
  949. subtree: true
  950. });
  951. }
  952.  
  953. if (contentContainer) {
  954. observer.observe(contentContainer, {
  955. childList: true,
  956. subtree: true
  957. });
  958. }
  959.  
  960. if (window.location.pathname === '/watch') {
  961. initializeButton();
  962. }
  963. }
  964.  
  965. startObserving();
  966.  
  967. if (!document.getElementById('player-container')) {
  968. const retryInterval = setInterval(() => {
  969. if (document.getElementById('player-container')) {
  970. startObserving();
  971. clearInterval(retryInterval);
  972. }
  973. }, 1000);
  974.  
  975. setTimeout(() => clearInterval(retryInterval), 10000);
  976. }
  977.  
  978. const handleNavigation = () => {
  979. if (window.location.pathname === '/watch') {
  980. initializeButton();
  981. }
  982. };
  983.  
  984. window.addEventListener('yt-navigate-finish', handleNavigation);
  985.  
  986. return () => {
  987. observer.disconnect();
  988. window.removeEventListener('yt-navigate-finish', handleNavigation);
  989. };
  990. }
  991.  
  992. function addSubtitleButton() {
  993. initializeObserver();
  994. }
  995.  
  996. addSubtitleButton();
  997. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址