您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download Subtitles in Various Languages.
// ==UserScript== // @name YouTube Enhancer (Subtitle Downloader) // @description Download Subtitles in Various Languages. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png // @version 1.4 // @author exyezed // @namespace https://github.com/exyezed/youtube-enhancer/ // @supportURL https://github.com/exyezed/youtube-enhancer/issues // @license MIT // @match https://www.youtube.com/* // @match https://youtube.com/* // @grant GM_xmlhttpRequest // @grant GM_download // @require https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js // @connect get-info.downsub.com // @connect download.subtitle.to // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const SECRET_KEY = "zthxw34cdp6wfyxmpad38v52t3hsz6c5"; const API = "https://get-info.downsub.com/"; const CryptoJS = window.CryptoJS; const GM_download = window.GM_download; const GM_xmlhttpRequest = window.GM_xmlhttpRequest; const formatJson = { stringify: function (crp) { let result = { ct: crp.ciphertext.toString(CryptoJS.enc.Base64) }; if (crp.iv) { result.iv = crp.iv.toString(); } if (crp.salt) { result.s = crp.salt.toString(); } return JSON.stringify(result); }, parse: function (output) { let parse = JSON.parse(output); let result = CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.enc.Base64.parse(parse.ct) }); if (parse.iv) { result.iv = CryptoJS.enc.Hex.parse(parse.iv); } if (parse.s) { result.salt = CryptoJS.enc.Hex.parse(parse.s); } return result; } }; function _toBase64(payload) { let vBtoa = btoa(payload); vBtoa = vBtoa.replace("+", "-"); vBtoa = vBtoa.replace("/", "_"); vBtoa = vBtoa.replace("=", ""); return vBtoa; } function _toBinary(base64) { let data = base64.replace("-", "+"); data = data.replace("_", "/"); const mod4 = data.length % 4; if (mod4) { data += "====".substring(mod4); } return atob(data); } function _encode(payload, options) { if (!payload) { return false; } let result = CryptoJS.AES.encrypt(JSON.stringify(payload), options || SECRET_KEY, { format: formatJson }).toString(); return _toBase64(result).trim(); } function _decode(payload, options) { if (!payload) { return false; } let result = CryptoJS.AES.decrypt(_toBinary(payload), options || SECRET_KEY, { format: formatJson }).toString(CryptoJS.enc.Utf8); return result.trim(); } function _generateData(videoId) { const url = `https://www.youtube.com/watch?v=${videoId}`; let id = videoId; return { state: 99, url: url, urlEncrypt: _encode(url), source: 0, id: _encode(id), playlistId: null }; } function _decodeArray(result) { let subtitles = [], subtitlesAutoTrans = []; if (result?.subtitles && result?.subtitles.length) { result.subtitles.forEach((v, i) => { let ff = {...v}; ff.url = _decode(ff.url).replace(/^"|"$/gi, ""); ff.enc_url = result.subtitles[i].url; ff.download = {}; const params = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url }); ff.download.srt = result.urlSubtitle + "?" + params.toString(); const params2 = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url, type: "txt" }); ff.download.txt = result.urlSubtitle + "?" + params2.toString(); const params3 = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url, type: "raw" }); ff.download.raw = result.urlSubtitle + "?" + params3.toString(); subtitles.push(ff); }); } if (result?.subtitlesAutoTrans && result?.subtitlesAutoTrans.length) { result.subtitlesAutoTrans.forEach((v, i) => { let ff = {...v}; ff.url = _decode(ff.url).replace(/^"|"$/gi, ""); ff.enc_url = result.subtitlesAutoTrans[i].url; ff.download = {}; const params = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url }); ff.download.srt = result.urlSubtitle + "?" + params.toString(); const params2 = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url, type: "txt" }); ff.download.txt = result.urlSubtitle + "?" + params2.toString(); const params3 = new URLSearchParams({ title: encodeURIComponent(ff.name), url: ff.enc_url, type: "raw" }); ff.download.raw = result.urlSubtitle + "?" + params3.toString(); subtitlesAutoTrans.push(ff); }); } return Object.assign(result, {subtitles}, {subtitlesAutoTrans}); } function createSVGIcon(className, isHover = false) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 576 512"); svg.classList.add(className); path.setAttribute("d", isHover ? "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" : "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" ); svg.appendChild(path); return svg; } function createSearchIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("width", "16"); svg.setAttribute("height", "16"); 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"); svg.appendChild(path); return svg; } function createCheckIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.classList.add("check-icon"); path.setAttribute("d", "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"); svg.appendChild(path); return svg; } function getVideoId() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('v'); } function downloadSubtitle(url, filename, format, buttonElement) { try { const buttonHeight = buttonElement.offsetHeight; const buttonWidth = buttonElement.offsetWidth; const originalChildren = Array.from(buttonElement.childNodes).map(node => node.cloneNode(true)); while (buttonElement.firstChild) { buttonElement.removeChild(buttonElement.firstChild); } buttonElement.style.height = `${buttonHeight}px`; buttonElement.style.width = `${buttonWidth}px`; const spinner = document.createElement('div'); spinner.className = 'button-spinner'; buttonElement.appendChild(spinner); buttonElement.disabled = true; GM_download({ url: url, name: filename, onload: function() { while (buttonElement.firstChild) { buttonElement.removeChild(buttonElement.firstChild); } buttonElement.appendChild(createCheckIcon()); buttonElement.classList.add('download-success'); setTimeout(() => { while (buttonElement.firstChild) { buttonElement.removeChild(buttonElement.firstChild); } originalChildren.forEach(child => { buttonElement.appendChild(child.cloneNode(true)); }); buttonElement.disabled = false; buttonElement.classList.remove('download-success'); buttonElement.style.height = ''; buttonElement.style.width = ''; }, 1500); }, onerror: function(error) { console.error('Download error:', error); while (buttonElement.firstChild) { buttonElement.removeChild(buttonElement.firstChild); } originalChildren.forEach(child => { buttonElement.appendChild(child.cloneNode(true)); }); buttonElement.disabled = false; buttonElement.style.height = ''; buttonElement.style.width = ''; } }); } catch (error) { console.error('Download setup error:', error); while (buttonElement.firstChild) { buttonElement.removeChild(buttonElement.firstChild); } buttonElement.textContent = format; buttonElement.disabled = false; buttonElement.style.height = ''; buttonElement.style.width = ''; } } function filterSubtitles(subtitles, query) { if (!query) return subtitles; const lowerQuery = query.toLowerCase(); return subtitles.filter(sub => sub.name.toLowerCase().includes(lowerQuery) ); } function createSubtitleTable(subtitles, autoTransSubs, videoTitle) { const container = document.createElement('div'); container.className = 'subtitle-container'; const titleDiv = document.createElement('div'); titleDiv.className = 'subtitle-dropdown-title'; titleDiv.textContent = `Download Subtitles (${subtitles.length + autoTransSubs.length})`; container.appendChild(titleDiv); const searchContainer = document.createElement('div'); searchContainer.className = 'subtitle-search-container'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = 'subtitle-search-input'; searchInput.placeholder = 'Search languages...'; const searchIcon = document.createElement('div'); searchIcon.className = 'subtitle-search-icon'; searchIcon.appendChild(createSearchIcon()); searchContainer.appendChild(searchIcon); searchContainer.appendChild(searchInput); container.appendChild(searchContainer); const tabsDiv = document.createElement('div'); tabsDiv.className = 'subtitle-tabs'; const regularTab = document.createElement('div'); regularTab.className = 'subtitle-tab active'; regularTab.textContent = 'Original'; regularTab.dataset.tab = 'regular'; const autoTab = document.createElement('div'); autoTab.className = 'subtitle-tab'; autoTab.textContent = 'Auto Translate'; autoTab.dataset.tab = 'auto'; tabsDiv.appendChild(regularTab); tabsDiv.appendChild(autoTab); container.appendChild(tabsDiv); const itemsPerPage = 30; const regularContent = createSubtitleContent(subtitles, videoTitle, true, itemsPerPage); regularContent.className = 'subtitle-content regular-content active'; const autoContent = createSubtitleContent(autoTransSubs, videoTitle, false, itemsPerPage); autoContent.className = 'subtitle-content auto-content'; container.appendChild(regularContent); container.appendChild(autoContent); tabsDiv.addEventListener('click', (e) => { if (e.target.classList.contains('subtitle-tab')) { document.querySelectorAll('.subtitle-tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.subtitle-content').forEach(content => content.classList.remove('active')); e.target.classList.add('active'); const tabType = e.target.dataset.tab; document.querySelector(`.${tabType}-content`).classList.add('active'); searchInput.value = ''; const activeContent = document.querySelector(`.${tabType}-content`); const grid = activeContent.querySelector('.subtitle-grid'); if (tabType === 'regular') { renderPage(1, subtitles, grid, itemsPerPage, videoTitle); } else { renderPage(1, autoTransSubs, grid, itemsPerPage, videoTitle); } const pagination = activeContent.querySelector('.subtitle-pagination'); updatePagination( 1, Math.ceil((tabType === 'regular' ? subtitles : autoTransSubs).length / itemsPerPage), pagination, null, grid, tabType === 'regular' ? subtitles : autoTransSubs, itemsPerPage, videoTitle ); } }); searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); const activeTab = document.querySelector('.subtitle-tab.active').dataset.tab; const activeContent = document.querySelector(`.${activeTab}-content`); const grid = activeContent.querySelector('.subtitle-grid'); const pagination = activeContent.querySelector('.subtitle-pagination'); const sourceSubtitles = activeTab === 'regular' ? subtitles : autoTransSubs; const filteredSubtitles = filterSubtitles(sourceSubtitles, query); renderPage(1, filteredSubtitles, grid, itemsPerPage, videoTitle); updatePagination( 1, Math.ceil(filteredSubtitles.length / itemsPerPage), pagination, filteredSubtitles, grid, sourceSubtitles, itemsPerPage, videoTitle ); grid.dataset.filteredCount = filteredSubtitles.length; grid.dataset.query = query; }); return container; } function renderPage(page, subtitlesList, gridElement, itemsPerPage, videoTitle) { while (gridElement.firstChild) { gridElement.removeChild(gridElement.firstChild); } const startIndex = (page - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, subtitlesList.length); for (let i = startIndex; i < endIndex; i++) { const sub = subtitlesList[i]; const item = document.createElement('div'); item.className = 'subtitle-item'; const langLabel = document.createElement('div'); langLabel.className = 'subtitle-language'; langLabel.textContent = sub.name; item.appendChild(langLabel); const btnContainer = document.createElement('div'); btnContainer.className = 'subtitle-format-container'; const srtBtn = document.createElement('button'); srtBtn.textContent = 'SRT'; srtBtn.className = 'subtitle-format-btn srt-btn'; srtBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); downloadSubtitle(sub.download.srt, `${videoTitle} - ${sub.name}.srt`, 'SRT', srtBtn); }); btnContainer.appendChild(srtBtn); const txtBtn = document.createElement('button'); txtBtn.textContent = 'TXT'; txtBtn.className = 'subtitle-format-btn txt-btn'; txtBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); downloadSubtitle(sub.download.txt, `${videoTitle} - ${sub.name}.txt`, 'TXT', txtBtn); }); btnContainer.appendChild(txtBtn); item.appendChild(btnContainer); gridElement.appendChild(item); } } function updatePagination(page, totalPages, paginationElement, filteredSubs, gridElement, sourceSubtitles, itemsPerPage, videoTitle) { while (paginationElement.firstChild) { paginationElement.removeChild(paginationElement.firstChild); } if (totalPages <= 1) return; const prevBtn = document.createElement('button'); prevBtn.textContent = '«'; prevBtn.className = 'pagination-btn'; prevBtn.disabled = page === 1; prevBtn.addEventListener('click', (e) => { e.stopPropagation(); if (page > 1) { const newPage = page - 1; const query = gridElement.dataset.query; const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles; renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle); updatePagination( newPage, totalPages, paginationElement, filteredSubs, gridElement, sourceSubtitles, itemsPerPage, videoTitle ); } }); paginationElement.appendChild(prevBtn); const pageIndicator = document.createElement('span'); pageIndicator.className = 'page-indicator'; pageIndicator.textContent = `${page} / ${totalPages}`; paginationElement.appendChild(pageIndicator); const nextBtn = document.createElement('button'); nextBtn.textContent = '»'; nextBtn.className = 'pagination-btn'; nextBtn.disabled = page === totalPages; nextBtn.addEventListener('click', (e) => { e.stopPropagation(); if (page < totalPages) { const newPage = page + 1; const query = gridElement.dataset.query; const subsToUse = query && filteredSubs ? filteredSubs : sourceSubtitles; renderPage(newPage, subsToUse, gridElement, itemsPerPage, videoTitle); updatePagination( newPage, totalPages, paginationElement, filteredSubs, gridElement, sourceSubtitles, itemsPerPage, videoTitle ); } }); paginationElement.appendChild(nextBtn); } function createSubtitleContent(subtitles, videoTitle, isOriginal, itemsPerPage) { const content = document.createElement('div'); let currentPage = 1; const grid = document.createElement('div'); grid.className = 'subtitle-grid'; if (isOriginal && subtitles.length <= 6) { grid.classList.add('center-grid'); } grid.dataset.filteredCount = subtitles.length; grid.dataset.query = ''; const pagination = document.createElement('div'); pagination.className = 'subtitle-pagination'; renderPage(currentPage, subtitles, grid, itemsPerPage, videoTitle); updatePagination( currentPage, Math.ceil(subtitles.length / itemsPerPage), pagination, null, grid, subtitles, itemsPerPage, videoTitle ); content.appendChild(grid); content.appendChild(pagination); return content; } async function handleSubtitleDownload(e) { e.preventDefault(); const videoId = getVideoId(); if (!videoId) { console.error('Video ID not found'); return; } const backdrop = document.createElement('div'); backdrop.className = 'subtitle-backdrop'; document.body.appendChild(backdrop); const loader = document.createElement('div'); loader.className = 'subtitle-loader'; backdrop.appendChild(loader); try { const data = _generateData(videoId); const headersList = { "authority": "get-info.downsub.com", "accept": "application/json, text/plain, */*", "accept-language": "id-ID,id;q=0.9", "origin": "https://downsub.com", "priority": "u=1, i", "referer": "https://downsub.com/", "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "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" }; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: API + data.id, headers: headersList, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`Request failed with status ${response.status}`)); } }, onerror: function() { reject(new Error('Network error')); } }); }); const processedResponse = _decodeArray(response); const videoTitleElement = document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata'); const videoTitle = videoTitleElement ? videoTitleElement.textContent.trim() : `youtube_video_${videoId}`; loader.remove(); if (!processedResponse.subtitles || processedResponse.subtitles.length === 0 && (!processedResponse.subtitlesAutoTrans || processedResponse.subtitlesAutoTrans.length === 0)) { while (backdrop.firstChild) { backdrop.removeChild(backdrop.firstChild); } const errorDiv = document.createElement('div'); errorDiv.className = 'subtitle-error'; errorDiv.textContent = 'No subtitles available for this video'; backdrop.appendChild(errorDiv); setTimeout(() => { backdrop.remove(); }, 2000); return; } const subtitleTable = createSubtitleTable( processedResponse.subtitles || [], processedResponse.subtitlesAutoTrans || [], videoTitle ); backdrop.appendChild(subtitleTable); backdrop.addEventListener('click', (e) => { if (!subtitleTable.contains(e.target)) { subtitleTable.remove(); backdrop.remove(); } }); subtitleTable.addEventListener('click', (e) => { e.stopPropagation(); }); } catch (error) { console.error('Error fetching subtitles:', error); while (backdrop.firstChild) { backdrop.removeChild(backdrop.firstChild); } const errorDiv = document.createElement('div'); errorDiv.className = 'subtitle-error'; errorDiv.textContent = 'Error fetching subtitles. Please try again.'; backdrop.appendChild(errorDiv); setTimeout(() => { backdrop.remove(); }, 2000); } } function initializeStyles(computedStyle) { if (document.querySelector('#yt-subtitle-downloader-styles')) return; const style = document.createElement('style'); style.id = 'yt-subtitle-downloader-styles'; style.textContent = ` .custom-subtitle-btn { background: none; border: none; cursor: pointer; padding: 0; width: ${computedStyle.width}; height: ${computedStyle.height}; display: flex; align-items: center; justify-content: center; position: relative; } @-moz-document url-prefix() { .custom-subtitle-btn { top: 0; margin-bottom: 0; vertical-align: top; } } .custom-subtitle-btn svg { width: 24px; height: 24px; fill: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); opacity: 1; transition: opacity 0.2s ease-in-out; } .custom-subtitle-btn .hover-icon { opacity: 0; } .custom-subtitle-btn:hover .default-icon { opacity: 0; } .custom-subtitle-btn:hover .hover-icon { opacity: 1; } .subtitle-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 9998; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(3px); } .subtitle-loader { width: 40px; height: 40px; border: 4px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top: 4px solid #fff; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .subtitle-error { background: rgba(0, 0, 0, 0.8); color: #fff; padding: 16px 24px; border-radius: 8px; font-size: 14px; } .subtitle-container { position: relative; background: rgba(28, 28, 28, 0.95); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 16px; z-index: 9999; min-width: 700px; max-width: 90vw; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); color: #fff; font-family: 'Roboto', Arial, sans-serif; } .subtitle-dropdown-title { color: #fff; font-size: 16px; font-weight: 500; margin-bottom: 16px; text-align: center; } .subtitle-search-container { position: relative; margin-bottom: 16px; width: 100%; max-width: 100%; } .subtitle-search-input { width: 100%; padding: 8px 12px 8px 36px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; font-size: 14px; box-sizing: border-box; } .subtitle-search-input::placeholder { color: rgba(255, 255, 255, 0.5); } .subtitle-search-input:focus { outline: none; border-color: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.15); } .subtitle-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; } .subtitle-search-icon svg { fill: rgba(255, 255, 255, 0.5); } .subtitle-tabs { display: flex; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 16px; justify-content: center; } .subtitle-tab { padding: 10px 20px; cursor: pointer; opacity: 0.7; transition: all 0.2s; border-bottom: 2px solid transparent; font-size: 15px; font-weight: 500; } .subtitle-tab:hover { opacity: 1; } .subtitle-tab.active { opacity: 1; border-bottom: 2px solid #2b7fff; } .subtitle-content { display: none; } .subtitle-content.active { display: block; } .subtitle-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; } .subtitle-grid.center-grid { justify-content: center; display: flex; flex-wrap: wrap; gap: 16px; } .center-grid .subtitle-item { width: 200px; } .subtitle-item { background: rgba(255, 255, 255, 0.05); border-radius: 6px; padding: 10px; transition: all 0.2s; } .subtitle-item:hover { background: rgba(255, 255, 255, 0.1); } .subtitle-language { font-size: 13px; font-weight: 500; margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .subtitle-format-container { display: flex; gap: 8px; } .subtitle-format-btn { flex: 1; padding: 6px 0; border-radius: 4px; border: none; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; text-align: center; position: relative; height: 28px; line-height: 16px; } .button-spinner { width: 14px; height: 14px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top: 2px solid #fff; animation: spin 1s linear infinite; margin: 0 auto; } .check-icon { width: 14px; height: 14px; fill: white; margin: 0 auto; } .download-success { background-color: #00a63e !important; } .srt-btn { background-color: #2b7fff; color: white; } .srt-btn:hover { background-color: #50a2ff; } .txt-btn { background-color: #615fff; color: white; } .txt-btn:hover { background-color: #7c86ff; } .subtitle-pagination { display: flex; justify-content: center; align-items: center; margin-top: 16px; } .pagination-btn { background: rgba(255, 255, 255, 0.1); border: none; color: white; width: 32px; height: 32px; border-radius: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s; } .pagination-btn:not(:disabled):hover { background: rgba(255, 255, 255, 0.2); } .pagination-btn:disabled { opacity: 0.3; cursor: not-allowed; } .page-indicator { margin: 0 16px; font-size: 14px; color: rgba(255, 255, 255, 0.7); } `; document.head.appendChild(style); } function initializeButton() { if (document.querySelector('.custom-subtitle-btn')) return; const originalButton = document.querySelector('.ytp-subtitles-button'); if (!originalButton) return; const newButton = document.createElement('button'); const computedStyle = window.getComputedStyle(originalButton); Object.assign(newButton, { className: 'ytp-button custom-subtitle-btn', title: 'Download Subtitles' }); newButton.setAttribute('aria-pressed', 'false'); initializeStyles(computedStyle); newButton.append( createSVGIcon('default-icon', false), createSVGIcon('hover-icon', true) ); newButton.addEventListener('click', (e) => { const existingDropdown = document.querySelector('.subtitle-container'); existingDropdown ? existingDropdown.remove() : handleSubtitleDownload(e); }); originalButton.insertAdjacentElement('afterend', newButton); } function initializeObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { const isVideoPage = window.location.pathname === '/watch'; if (isVideoPage && !document.querySelector('.custom-subtitle-btn')) { initializeButton(); } } }); }); function startObserving() { const playerContainer = document.getElementById('player-container'); const contentContainer = document.getElementById('content'); if (playerContainer) { observer.observe(playerContainer, { childList: true, subtree: true }); } if (contentContainer) { observer.observe(contentContainer, { childList: true, subtree: true }); } if (window.location.pathname === '/watch') { initializeButton(); } } startObserving(); if (!document.getElementById('player-container')) { const retryInterval = setInterval(() => { if (document.getElementById('player-container')) { startObserving(); clearInterval(retryInterval); } }, 1000); setTimeout(() => clearInterval(retryInterval), 10000); } const handleNavigation = () => { if (window.location.pathname === '/watch') { initializeButton(); } }; window.addEventListener('yt-navigate-finish', handleNavigation); return () => { observer.disconnect(); window.removeEventListener('yt-navigate-finish', handleNavigation); }; } function addSubtitleButton() { initializeObserver(); } addSubtitleButton(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址