Greasy Fork is available in English.
Native dual subtitles for YouTube
// ==UserScript==
// @name YouTube Dual Native Subs
// @namespace https://github.com/luismusaj646-prog/dual-subtitles
// @version 4.1.0
// @description Native dual subtitles for YouTube
// @license GPL-3.0-only
// @homepageURL https://github.com/luismusaj646-prog/dual-subtitles
// @supportURL https://github.com/luismusaj646-prog/dual-subtitles/issues
// @match https://www.youtube.com/watch*
// @run-at document-idle
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect www.youtube.com
// ==/UserScript==
(function () {
'use strict';
var SCRIPT_NAME = 'yt-dual-subs';
var SCRIPT_VERSION = '4.1.0';
var SETTINGS_KEY = 'yds_native_settings_v2';
var RUNTIME_KEY = '__ydsRuntime';
var DEBUG_API_KEY = '__ydsDebug';
var LOG_PREFIX = '[tm-script][' + SCRIPT_NAME + ']';
var TRANSCRIPT_LABEL_PATTERN = /(show transcript|open transcript|transcript|字幕记录|字幕記錄|文字稿|逐字稿|转录稿|轉錄稿|transkript anzeigen|transkript öffnen|mostrar transcripci[oó]n|abrir transcripci[oó]n|показать расшифровку видео|расшифровка)/i;
var CONFIG = {
initDelayMs: 320,
domDebounceMs: 180,
retryDelayMs: 1200,
routePollMs: 1200,
maxTrackRetries: 8,
nativeTimedTextHintWaitMs: 2200,
nativeTimedTextParamKeys: [
'potc',
'pot',
'xorb',
'xobt',
'xovt',
'cbr',
'cbrver',
'c',
'cver',
'cplayer',
'cos',
'cosver',
'cplatform'
],
rateLimitBackoffMs: 60000,
selectors: {
watchPath: '/watch',
rootVideo: 'video',
player: '.html5-video-player',
captionContainer: '.ytp-caption-window-container',
playerControls: '.ytp-chrome-bottom',
nativeCaptionText: '.caption-window .ytp-caption-segment, .ytp-caption-segment, .caption-visual-line',
subtitlesButton: '.ytp-subtitles-button',
transcriptPanel: 'ytd-engagement-panel-section-list-renderer',
transcriptRenderer: 'ytd-transcript-renderer',
transcriptSegment: 'ytd-transcript-segment-renderer, transcript-segment-view-model',
transcriptText: '.segment-text, yt-formatted-string, .yt-core-attributed-string[role="text"]',
transcriptTime: '.segment-timestamp, .ytwTranscriptSegmentViewModelTimestamp',
transcriptChipButton: 'button[aria-label], yt-button-shape button[aria-label], button[title], [role="button"][aria-label]',
transcriptMenuButton: 'ytd-menu-renderer :is(yt-button-shape button, button#button.style-scope.ytd-menu-renderer, ytd-video-primary-info-renderer button, button[aria-haspopup=\"true\"])[aria-label*=\"more actions\" i], ytd-button-renderer button:is([aria-label*=\"transcript\" i],[title*=\"transcript\" i])',
transcriptMenuItems: 'ytd-menu-service-item-renderer, tp-yt-paper-item, yt-formatted-string.style-scope.ytd-menu-service-item-renderer',
transcriptDescriptionButton: 'button[aria-label*=\"transcript\" i], button[aria-label*=\"字幕\" i], button[aria-label*=\"文字稿\" i], button[title*=\"transcript\" i], #description-inline-expander [aria-label*=\"transcript\" i]',
transcriptLanguageDropdown: 'ytd-transcript-footer-renderer yt-dropdown-menu tp-yt-paper-button, ytd-transcript-footer-renderer yt-dropdown-menu button',
transcriptVisibleListboxes: 'tp-yt-iron-dropdown:not([aria-hidden=\"true\"]) tp-yt-paper-listbox',
metadataTopRow: 'ytd-watch-metadata #top-row, #above-the-fold #top-row',
metadataActions: 'ytd-watch-metadata #actions, #above-the-fold #actions',
metadataActionButtons: 'ytd-watch-metadata #actions #top-level-buttons-computed, #above-the-fold #actions #top-level-buttons-computed, ytd-watch-metadata #actions, #above-the-fold #actions'
},
ids: {
uiSlot: 'yds-page-slot',
launcher: 'yds-launcher-root',
panel: 'yds-panel-root',
nativeWindow: 'yds-native-window',
debugBox: 'yds-debug-box',
hiddenTranscriptStyle: 'yds-hidden-transcript-style'
},
historyEventName: 'yds-history-change',
debugQueryParam: 'ydsDebug=1',
defaultDebug: false
};
var DEFAULTS = {
targetLang: 'zh-Hans',
targetLangBySource: {},
sourceTrackIndex: 0,
enabled: true,
displayMode: 'dual',
panelOpen: false,
debug: CONFIG.defaultDebug,
launcherPosition: null,
panelPosition: null,
sourceFontSize: 28,
targetFontSize: 28,
lineGap: 6,
bottomOffset: 9,
sourceColor: '#ffffff',
targetColor: '#00e5ff',
fontFamily: 'system',
smartPosition: true,
syncNativeStyle: true
};
var FONT_OPTIONS = [
{ value: 'system', label: '\u7CFB\u7EDF\u9ED8\u8BA4', css: 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif' },
{ value: 'youtube', label: 'YouTube Sans', css: '"YouTube Sans","Roboto",Arial,sans-serif' },
{ value: 'arial', label: 'Arial', css: 'Arial,"Helvetica Neue",sans-serif' },
{ value: 'roboto', label: 'Roboto', css: '"Roboto",Arial,sans-serif' },
{ value: 'segoe', label: 'Segoe UI', css: '"Segoe UI",Arial,sans-serif' },
{ value: 'microsoft-yahei', label: '\u5FAE\u8F6F\u96C5\u9ED1', css: '"Microsoft YaHei","Segoe UI",Arial,sans-serif' },
{ value: 'noto-sans', label: 'Noto Sans', css: '"Noto Sans","Noto Sans SC",Arial,sans-serif' },
{ value: 'serif', label: '\u886C\u7EBF', css: 'Georgia,"Times New Roman",serif' },
{ value: 'mono', label: '\u7B49\u5BBD', css: '"Cascadia Mono","Consolas",monospace' }
];
var TEXT = {
title: '\u53CC\u5B57\u5E55',
launcher: 'X',
reload: '\u91CD\u8F7D',
sourceTrack: '\u5F53\u524D\u539F\u8F68',
displayMode: '\u663E\u793A\u6A21\u5F0F',
modeDual: '\u539F\u6587 + \u8BD1\u6587',
modeSource: '\u53EA\u663E\u793A\u539F\u6587',
modeTarget: '\u53EA\u663E\u793A\u8BD1\u6587',
targetLang: '\u76EE\u6807\u8BED\u8A00',
targetSearch: '\u641C\u7D22\u8BED\u8A00',
targetSearchPlaceholder: '\u8F93\u5165\u8BED\u8A00\u6216\u4EE3\u7801',
trackIndex: '\u539F\u5B57\u5E55\u8F68',
styleTitle: '\u5B57\u5E55\u6837\u5F0F',
sourceFontSize: '\u539F\u6587\u5B57\u53F7',
targetFontSize: '\u8BD1\u6587\u5B57\u53F7',
lineGap: '\u884C\u95F4\u8DDD',
bottomOffset: '\u5E95\u90E8\u4F4D\u7F6E',
fontFamily: '\u5B57\u4F53',
sourceColor: '\u539F\u6587\u989C\u8272',
targetColor: '\u8BD1\u6587\u989C\u8272',
advancedTitle: '\u9AD8\u7EA7',
resetStyle: '\u91CD\u7F6E\u6837\u5F0F',
debug: 'debug',
injected: '\u811A\u672C\u5DF2\u6CE8\u5165',
waitingWatchPage: '\u7B49\u5F85 watch \u9875\u9762',
waitingPlayer: '\u7B49\u5F85\u64AD\u653E\u5668\u5B8C\u6210\u52A0\u8F7D',
waitingTracks: '\u7B49\u5F85\u5B57\u5E55\u8F68\u51FA\u73B0',
loading: '\u6B63\u5728\u52A0\u8F7D...',
noTrack: '\u65E0\u53EF\u7528\u5B57\u5E55\u8F68',
noTrackDetail: '\u8FD9\u4E2A\u89C6\u9891\u6CA1\u6709\u53EF\u7528\u5B57\u5E55\u8F68',
noCue: '\u5B57\u5E55\u6CA1\u62FF\u5230\u6709\u6548\u5185\u5BB9',
nativeReady: '\u53CC\u5B57\u5E55\u5DF2\u542F\u7528',
sourceOnly: '\u53EA\u62FF\u5230\u539F\u5B57\u5E55',
rateLimited: '\u7FFB\u8BD1\u88AB\u9650\u6D41\uFF0C60\u79D2\u540E\u518D\u8BD5',
rateLimitedShort: '\u7FFB\u8BD1\u88AB\u9650\u6D41\uFF0C\u7A0D\u540E\u518D\u8BD5',
loadFailed: '\u52A0\u8F7D\u5931\u8D25: ',
disabled: '\u53CC\u5B57\u5E55\u5DF2\u5173\u95ED',
enableDualSubs: '\u5F00\u542F\u53CC\u5B57\u5E55',
disableDualSubs: '\u5173\u95ED\u53CC\u5B57\u5E55',
unselected: '\u672A\u9009\u62E9'
};
if (window[RUNTIME_KEY] && typeof window[RUNTIME_KEY].destroy === 'function') {
window[RUNTIME_KEY].destroy('reinject');
}
GM_addStyle(
'.yds-page-slot{' +
'position:relative;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;height:36px;' +
'margin:0 0 0 8px;z-index:2200;vertical-align:middle;' +
'}' +
'.yds-launcher{' +
'position:static;z-index:2200;width:36px;height:36px;min-width:36px;padding:0;border:0;border-radius:18px;' +
'display:flex;align-items:center;justify-content:center;background:var(--yt-spec-badge-chip-background,#f2f2f2);' +
'color:var(--yt-spec-text-primary,#0f0f0f);font:500 14px/36px Roboto,Arial,sans-serif;cursor:pointer;user-select:none;' +
'box-shadow:none;outline:0;' +
'}' +
'.yds-launcher:hover,.yds-launcher[aria-expanded=\"true\"]{background:var(--yt-spec-mono-tonal-hover,#e5e5e5);}' +
'.yds-launcher:focus-visible{box-shadow:0 0 0 2px var(--yt-spec-themed-blue,#065fd4);}' +
'.yds-launcher.yds-detached{position:fixed;top:16px;right:16px;background:var(--yt-spec-badge-chip-background,#f2f2f2);}' +
'.yds-panel{' +
'position:absolute;top:44px;right:0;z-index:2201;width:360px;max-width:min(360px,calc(100vw - 24px));' +
'max-height:min(78vh,640px);overflow:auto;box-sizing:border-box;padding:8px 0;border:0;border-radius:12px;' +
'background:var(--yt-spec-menu-background,var(--yt-spec-base-background,#fff));color:var(--yt-spec-text-primary,#0f0f0f);' +
'font:400 14px/20px Roboto,Arial,sans-serif;box-shadow:0 4px 32px rgba(0,0,0,.16);color-scheme:light dark;' +
'}' +
'.yds-panel.yds-detached{position:fixed;top:56px;right:16px;max-height:78vh;}' +
'.yds-panel[dir=\"ltr\"]{right:0;left:auto;}' +
'.yds-panel input,.yds-panel button,.yds-panel select{font:inherit;}' +
'.yds-panel input,.yds-panel select{box-sizing:border-box;}' +
'.yds-panel input[type="text"],.yds-panel input[type="number"],.yds-panel select{' +
'height:36px;border-radius:8px;border:1px solid var(--yt-spec-10-percent-layer,rgba(0,0,0,.1));' +
'background:var(--yt-spec-base-background,#fff);color:var(--yt-spec-text-primary,#0f0f0f);padding:0 32px 0 12px;' +
'}' +
'.yds-panel select{width:100%;}' +
'.yds-panel option{background:var(--yt-spec-menu-background,var(--yt-spec-base-background,#fff));color:var(--yt-spec-text-primary,#0f0f0f);}' +
'.yds-panel input[type="color"]{width:36px;height:36px;padding:0;border:0;background:transparent;}' +
'.yds-panel input[type="range"]{min-width:0;accent-color:var(--yt-spec-themed-blue,#065fd4);}' +
'.yds-panel button{height:36px;border-radius:18px;border:0;background:var(--yt-spec-badge-chip-background,#f2f2f2);color:var(--yt-spec-text-primary,#0f0f0f);cursor:pointer;padding:0 14px;}' +
'.yds-panel button:hover{background:var(--yt-spec-mono-tonal-hover,#e5e5e5);}' +
'.yds-panel label,.yds-field,.yds-select-field{' +
'display:grid;grid-template-columns:112px minmax(0,1fr);align-items:center;gap:12px;min-height:48px;' +
'box-sizing:border-box;margin:0;padding:6px 16px;color:var(--yt-spec-text-primary,#0f0f0f);' +
'}' +
'.yds-panel label:hover,.yds-field:hover,.yds-select-field:hover{background:var(--yt-spec-mono-tonal-hover,rgba(0,0,0,.06));}' +
'.yds-section-title{margin:8px 0 0;padding:10px 16px 6px;font:500 14px/20px Roboto,Arial,sans-serif;color:var(--yt-spec-text-primary,#0f0f0f);}' +
'.yds-field{grid-template-columns:112px minmax(0,1fr) 56px;}' +
'.yds-field>span,.yds-select-field>span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' +
'.yds-field input[type="number"]{width:56px;text-align:center;padding:0 6px;}' +
'.yds-field-unit{font-size:12px;color:var(--yt-spec-text-secondary,#606060);margin-left:2px;}' +
'.yds-row{display:flex;gap:8px;align-items:center;min-height:40px;padding:4px 16px;}' +
'.yds-row>*{flex:1;}' +
'.yds-title-row{border-bottom:1px solid var(--yt-spec-10-percent-layer,rgba(0,0,0,.1));margin-bottom:4px;padding-bottom:8px;}' +
'.yds-title-row strong{font:500 16px/22px Roboto,Arial,sans-serif;}' +
'.yds-close-btn{flex:0 0 36px !important;padding:0 !important;}' +
'.yds-enabled-btn{width:calc(100% - 32px);margin:6px 16px;}' +
'.yds-toggle{display:flex;align-items:center;gap:12px;min-height:40px;margin:0;padding:6px 16px;}' +
'.yds-toggle:hover{background:var(--yt-spec-mono-tonal-hover,rgba(0,0,0,.06));}' +
'.yds-toggle input{flex:0 0 auto;}' +
'.yds-advanced{margin:4px 0 0;border-top:1px solid var(--yt-spec-10-percent-layer,rgba(0,0,0,.1));}' +
'.yds-advanced summary{cursor:pointer;list-style:none;min-height:40px;padding:10px 16px;box-sizing:border-box;color:var(--yt-spec-text-primary,#0f0f0f);}' +
'.yds-advanced summary::-webkit-details-marker{display:none;}' +
'.yds-advanced summary:hover{background:var(--yt-spec-mono-tonal-hover,rgba(0,0,0,.06));}' +
'.yds-status{padding:6px 16px;font-size:12px;line-height:18px;color:var(--yt-spec-text-secondary,#606060);white-space:pre-wrap;word-break:break-word;}' +
'.yds-debug{margin:6px 16px 10px;max-height:220px;overflow:auto;padding:8px;border-radius:8px;background:var(--yt-spec-badge-chip-background,#f2f2f2);font:12px/16px Consolas,monospace;white-space:pre-wrap;word-break:break-word;color:var(--yt-spec-text-primary,#0f0f0f);}' +
'.yds-debug[hidden]{display:none;}' +
'html[dark] .yds-launcher,body[dark] .yds-launcher,ytd-app[dark] .yds-launcher,.dark .yds-launcher{' +
'background:#272727;color:#f1f1f1;' +
'}' +
'html[dark] .yds-launcher:hover,html[dark] .yds-launcher[aria-expanded=\"true\"],body[dark] .yds-launcher:hover,body[dark] .yds-launcher[aria-expanded=\"true\"],ytd-app[dark] .yds-launcher:hover,ytd-app[dark] .yds-launcher[aria-expanded=\"true\"],.dark .yds-launcher:hover,.dark .yds-launcher[aria-expanded=\"true\"]{' +
'background:#3f3f3f;' +
'}' +
'html[dark] .yds-panel,body[dark] .yds-panel,ytd-app[dark] .yds-panel,.dark .yds-panel{' +
'background:#282828;color:#f1f1f1;box-shadow:0 4px 32px rgba(0,0,0,.48);color-scheme:dark;' +
'}' +
'html[dark] .yds-panel input[type="text"],html[dark] .yds-panel input[type="number"],html[dark] .yds-panel select,body[dark] .yds-panel input[type="text"],body[dark] .yds-panel input[type="number"],body[dark] .yds-panel select,ytd-app[dark] .yds-panel input[type="text"],ytd-app[dark] .yds-panel input[type="number"],ytd-app[dark] .yds-panel select,.dark .yds-panel input[type="text"],.dark .yds-panel input[type="number"],.dark .yds-panel select{' +
'background:#121212;color:#f1f1f1;border-color:#3f3f3f;' +
'}' +
'html[dark] .yds-panel option,body[dark] .yds-panel option,ytd-app[dark] .yds-panel option,.dark .yds-panel option{' +
'background:#282828;color:#f1f1f1;' +
'}' +
'html[dark] .yds-panel button,body[dark] .yds-panel button,ytd-app[dark] .yds-panel button,.dark .yds-panel button{' +
'background:#3f3f3f;color:#f1f1f1;' +
'}' +
'html[dark] .yds-panel button:hover,body[dark] .yds-panel button:hover,ytd-app[dark] .yds-panel button:hover,.dark .yds-panel button:hover{' +
'background:#535353;' +
'}' +
'html[dark] .yds-title-row,html[dark] .yds-advanced,body[dark] .yds-title-row,body[dark] .yds-advanced,ytd-app[dark] .yds-title-row,ytd-app[dark] .yds-advanced,.dark .yds-title-row,.dark .yds-advanced{' +
'border-color:#3f3f3f;' +
'}' +
'html[dark] .yds-panel label,html[dark] .yds-field,html[dark] .yds-select-field,html[dark] .yds-section-title,html[dark] .yds-advanced summary,body[dark] .yds-panel label,body[dark] .yds-field,body[dark] .yds-select-field,body[dark] .yds-section-title,body[dark] .yds-advanced summary,ytd-app[dark] .yds-panel label,ytd-app[dark] .yds-field,ytd-app[dark] .yds-select-field,ytd-app[dark] .yds-section-title,ytd-app[dark] .yds-advanced summary,.dark .yds-panel label,.dark .yds-field,.dark .yds-select-field,.dark .yds-section-title,.dark .yds-advanced summary{' +
'color:#f1f1f1;' +
'}' +
'html[dark] .yds-panel label:hover,html[dark] .yds-field:hover,html[dark] .yds-select-field:hover,html[dark] .yds-toggle:hover,html[dark] .yds-advanced summary:hover,body[dark] .yds-panel label:hover,body[dark] .yds-field:hover,body[dark] .yds-select-field:hover,body[dark] .yds-toggle:hover,body[dark] .yds-advanced summary:hover,ytd-app[dark] .yds-panel label:hover,ytd-app[dark] .yds-field:hover,ytd-app[dark] .yds-select-field:hover,ytd-app[dark] .yds-toggle:hover,ytd-app[dark] .yds-advanced summary:hover,.dark .yds-panel label:hover,.dark .yds-field:hover,.dark .yds-select-field:hover,.dark .yds-toggle:hover,.dark .yds-advanced summary:hover{' +
'background:#3f3f3f;' +
'}' +
'html[dark] .yds-status,html[dark] .yds-field-unit,body[dark] .yds-status,body[dark] .yds-field-unit,ytd-app[dark] .yds-status,ytd-app[dark] .yds-field-unit,.dark .yds-status,.dark .yds-field-unit{' +
'color:#aaa;' +
'}' +
'html[dark] .yds-debug,body[dark] .yds-debug,ytd-app[dark] .yds-debug,.dark .yds-debug{' +
'background:#1f1f1f;color:#f1f1f1;' +
'}' +
'.ytp-caption-window-container.yds-native-mode .caption-window{display:none !important;}' +
'.html5-video-player .yds-native-window{' +
'position:absolute;left:0;right:0;bottom:9%;z-index:63;padding:0 24px;box-sizing:border-box;' +
'display:flex;flex-direction:column;align-items:center;text-align:center;pointer-events:none;' +
'text-shadow:0 2px 4px rgba(0,0,0,.85);' +
'}' +
'.html5-video-player .yds-native-line{' +
'display:block;max-width:100%;font:600 28px/1.24 system-ui,sans-serif;white-space:pre-line;word-break:break-word;' +
'}' +
'.html5-video-player .yds-native-line-b{margin-top:6px;color:#00e5ff;}'
);
var state = loadSettings();
var logger = createLogger(function () {
return isDebugEnabled(state);
});
var fetchDiagnostics = {
source: '',
target: ''
};
var nativeTimedTextHints = {
videoId: '',
byLang: {},
last: null
};
var runtime = createRuntime();
window[RUNTIME_KEY] = runtime;
runtime.boot();
function createRuntime() {
var app = {
activeRequestId: 0,
backoffUntil: 0,
cuesA: [],
cuesB: [],
defaultTrackIndex: -1,
lastSourceName: '',
lastCaptionSource: '',
lastFallback: '',
lastVideoId: '',
lastUrl: '',
loading: false,
loopId: 0,
pendingLoadKey: '',
phase: 'boot',
status: '',
translationLanguages: [],
timers: {
init: 0,
urlPoll: 0
},
tracks: [],
trackRetryCount: 0,
teardown: [],
observer: null
};
var ui = {
launcher: null,
panel: null,
sourceName: null,
status: null,
displayMode: null,
targetSearch: null,
targetLang: null,
trackIndex: null,
sourceFontSize: null,
targetFontSize: null,
lineGap: null,
bottomOffset: null,
fontFamily: null,
sourceColor: null,
targetColor: null,
enabledBtn: null,
debugToggle: null,
debugBox: null
};
function boot() {
setPhase('boot');
installTimedTextObserver();
buildUi();
mountUi();
exposeDebugApi();
bindGlobalListeners();
setStatus(TEXT.injected);
logger.debug('boot', collectSnapshot());
scheduleInit('boot', 80);
}
function destroy(reason) {
clearTimeout(app.timers.init);
clearInterval(app.timers.urlPoll);
if (app.observer) app.observer.disconnect();
stopLoop();
clearNativeCaptionWindow();
unmountUi();
while (app.teardown.length) {
try {
app.teardown.pop()();
} catch (err) {
logger.error('teardown failed', err);
}
}
if (getPageWindow()[DEBUG_API_KEY] && getPageWindow()[DEBUG_API_KEY].runtime === api) {
delete getPageWindow()[DEBUG_API_KEY];
}
if (window[RUNTIME_KEY] === api) {
delete window[RUNTIME_KEY];
}
logger.debug('destroy', { reason: reason || 'unknown' });
}
function buildUi() {
if (ui.launcher && ui.panel) return;
ui.launcher = document.createElement('button');
ui.launcher.id = CONFIG.ids.launcher;
ui.launcher.type = 'button';
ui.launcher.className = 'yds-launcher';
ui.launcher.textContent = TEXT.launcher;
ui.launcher.title = TEXT.title;
ui.launcher.addEventListener('click', function () {
state.panelOpen = !state.panelOpen;
saveSettings(state);
mountUi();
});
ui.panel = document.createElement('div');
ui.panel.id = CONFIG.ids.panel;
ui.panel.className = 'yds-panel';
ui.panel.dir = 'ltr';
var titleRow = document.createElement('div');
titleRow.className = 'yds-row';
titleRow.className += ' yds-title-row';
var title = document.createElement('strong');
title.textContent = TEXT.title;
var reloadBtn = document.createElement('button');
reloadBtn.type = 'button';
reloadBtn.textContent = TEXT.reload;
reloadBtn.addEventListener('click', function () {
reloadDualSubsSoon('manual-reload');
});
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'yds-close-btn';
closeBtn.textContent = 'x';
closeBtn.title = '\u9690\u85CF';
closeBtn.addEventListener('click', function () {
state.panelOpen = false;
saveSettings(state);
mountUi();
});
titleRow.appendChild(title);
titleRow.appendChild(reloadBtn);
titleRow.appendChild(closeBtn);
ui.panel.appendChild(titleRow);
ui.enabledBtn = document.createElement('button');
ui.enabledBtn.type = 'button';
ui.enabledBtn.className = 'yds-enabled-btn';
ui.enabledBtn.addEventListener('click', function () {
setDualSubsEnabled(!state.enabled, 'toggle-button');
});
ui.panel.appendChild(ui.enabledBtn);
ui.displayMode = createDisplayModeField();
var sourceLabel = document.createElement('label');
sourceLabel.textContent = TEXT.sourceTrack;
ui.sourceName = document.createElement('div');
ui.sourceName.className = 'yds-status';
sourceLabel.appendChild(ui.sourceName);
ui.panel.appendChild(sourceLabel);
var targetSearchLabel = document.createElement('label');
targetSearchLabel.textContent = TEXT.targetSearch;
ui.targetSearch = document.createElement('input');
ui.targetSearch.type = 'text';
ui.targetSearch.placeholder = TEXT.targetSearchPlaceholder;
ui.targetSearch.setAttribute('data-yds-control', 'target-lang-search');
ui.targetSearch.addEventListener('input', function () {
syncTargetLanguageOptions(true);
});
targetSearchLabel.appendChild(ui.targetSearch);
ui.panel.appendChild(targetSearchLabel);
var targetLabel = document.createElement('label');
targetLabel.textContent = TEXT.targetLang;
ui.targetLang = document.createElement('select');
ui.targetLang.setAttribute('data-yds-control', 'target-lang');
ui.targetLang.addEventListener('change', function () {
state.targetLang = String(ui.targetLang.value || DEFAULTS.targetLang).trim() || DEFAULTS.targetLang;
rememberTargetForCurrentSource();
saveSettings(state);
reloadDualSubsSoon('target-lang-change');
});
targetLabel.appendChild(ui.targetLang);
ui.panel.appendChild(targetLabel);
var trackLabel = document.createElement('label');
trackLabel.textContent = TEXT.trackIndex;
ui.trackIndex = document.createElement('select');
ui.trackIndex.setAttribute('data-yds-control', 'source-track-index');
ui.trackIndex.addEventListener('change', function () {
var next = parseInt(ui.trackIndex.value || '0', 10);
state.sourceTrackIndex = isNaN(next) ? 0 : Math.max(0, next);
applyRememberedTargetForTrack(app.tracks[state.sourceTrackIndex], true);
saveSettings(state);
reloadDualSubsSoon('track-index-change');
});
trackLabel.appendChild(ui.trackIndex);
ui.panel.appendChild(trackLabel);
var styleTitle = document.createElement('div');
styleTitle.className = 'yds-section-title';
styleTitle.textContent = TEXT.styleTitle;
ui.panel.appendChild(styleTitle);
ui.sourceFontSize = createNumberRangeField(TEXT.sourceFontSize, 'sourceFontSize', 16, 56, 1, 'px');
ui.targetFontSize = createNumberRangeField(TEXT.targetFontSize, 'targetFontSize', 16, 56, 1, 'px');
ui.lineGap = createNumberRangeField(TEXT.lineGap, 'lineGap', 0, 24, 1, 'px');
ui.bottomOffset = createNumberRangeField(TEXT.bottomOffset, 'bottomOffset', 2, 28, 1, '%');
ui.fontFamily = createFontField();
ui.sourceColor = createColorField(TEXT.sourceColor, 'sourceColor');
ui.targetColor = createColorField(TEXT.targetColor, 'targetColor');
var resetStyleBtn = document.createElement('button');
resetStyleBtn.type = 'button';
resetStyleBtn.textContent = TEXT.resetStyle;
resetStyleBtn.addEventListener('click', function () {
resetSubtitleStyle();
});
ui.panel.appendChild(resetStyleBtn);
var advanced = document.createElement('details');
advanced.className = 'yds-advanced';
var advancedSummary = document.createElement('summary');
advancedSummary.textContent = TEXT.advancedTitle;
advanced.appendChild(advancedSummary);
var toggleRow = document.createElement('label');
toggleRow.className = 'yds-toggle';
ui.debugToggle = document.createElement('input');
ui.debugToggle.type = 'checkbox';
ui.debugToggle.addEventListener('change', function () {
state.debug = !!ui.debugToggle.checked;
saveSettings(state);
logger.debug('debug toggled', { enabled: state.debug });
syncUi();
});
var toggleText = document.createElement('span');
toggleText.textContent = TEXT.debug;
toggleRow.appendChild(ui.debugToggle);
toggleRow.appendChild(toggleText);
advanced.appendChild(toggleRow);
ui.status = document.createElement('div');
ui.status.className = 'yds-status';
ui.panel.appendChild(ui.status);
ui.debugBox = document.createElement('div');
ui.debugBox.id = CONFIG.ids.debugBox;
ui.debugBox.className = 'yds-debug';
advanced.appendChild(ui.debugBox);
ui.panel.appendChild(advanced);
syncUi();
}
function createDisplayModeField() {
var row = document.createElement('label');
row.textContent = TEXT.displayMode;
var select = document.createElement('select');
select.setAttribute('data-yds-control', 'display-mode');
[
{ value: 'dual', label: TEXT.modeDual },
{ value: 'source', label: TEXT.modeSource },
{ value: 'target', label: TEXT.modeTarget }
].forEach(function (option) {
var node = document.createElement('option');
node.value = option.value;
node.textContent = option.label;
select.appendChild(node);
});
select.addEventListener('change', function () {
state.displayMode = normalizeDisplayMode(select.value);
saveSettings(state);
renderCurrentCaption();
syncUi();
});
row.appendChild(select);
ui.panel.appendChild(row);
return select;
}
function createNumberRangeField(labelText, stateKey, min, max, step, unit) {
var row = document.createElement('div');
row.className = 'yds-field';
var label = document.createElement('span');
label.textContent = labelText;
var range = document.createElement('input');
range.type = 'range';
range.min = String(min);
range.max = String(max);
range.step = String(step);
var number = document.createElement('input');
number.type = 'number';
number.min = String(min);
number.max = String(max);
number.step = String(step);
number.setAttribute('data-yds-control', stateKey);
function commit(value) {
state[stateKey] = clampNumber(parseFloat(value), min, max, DEFAULTS[stateKey]);
range.value = String(state[stateKey]);
number.value = String(state[stateKey]);
saveSettings(state);
applySubtitleStyle();
}
range.addEventListener('input', function () {
commit(range.value);
});
number.addEventListener('change', function () {
commit(number.value);
});
row.appendChild(label);
row.appendChild(range);
row.appendChild(number);
ui.panel.appendChild(row);
return {
number: number,
range: range,
unit: unit
};
}
function createFontField() {
var row = document.createElement('div');
row.className = 'yds-select-field';
var label = document.createElement('span');
label.textContent = TEXT.fontFamily;
var select = document.createElement('select');
select.setAttribute('data-yds-control', 'font-family');
FONT_OPTIONS.forEach(function (option) {
var node = document.createElement('option');
node.value = option.value;
node.textContent = option.label;
select.appendChild(node);
});
select.addEventListener('change', function () {
state.fontFamily = normalizeFontFamily(select.value);
saveSettings(state);
applySubtitleStyle();
});
row.appendChild(label);
row.appendChild(select);
ui.panel.appendChild(row);
return select;
}
function createColorField(labelText, stateKey) {
var row = document.createElement('div');
row.className = 'yds-field';
var label = document.createElement('span');
label.textContent = labelText;
var preview = document.createElement('span');
preview.className = 'yds-field-unit';
preview.textContent = '\u25CF';
var color = document.createElement('input');
color.type = 'color';
color.setAttribute('data-yds-control', stateKey);
color.addEventListener('input', function () {
state[stateKey] = normalizeColor(color.value, DEFAULTS[stateKey]);
saveSettings(state);
applySubtitleStyle();
syncColorPreview(preview, state[stateKey]);
});
row.appendChild(label);
row.appendChild(preview);
row.appendChild(color);
ui.panel.appendChild(row);
return {
input: color,
preview: preview
};
}
function mountUi() {
if (!isWatchPage()) {
unmountUi();
return;
}
var host = ensurePageControlsSlot();
if (!host) {
unmountUi();
syncUi();
return;
}
var detached = false;
setDetachedClass(ui.launcher, detached);
setDetachedClass(ui.panel, detached);
if (ui.launcher) ui.launcher.setAttribute('aria-expanded', state.panelOpen ? 'true' : 'false');
if (ui.launcher && ui.launcher.parentNode !== host) host.appendChild(ui.launcher);
if (state.panelOpen) {
if (ui.panel && ui.panel.parentNode !== host) host.appendChild(ui.panel);
} else if (ui.panel && ui.panel.isConnected) {
ui.panel.remove();
}
if (detached) {
applyStoredPosition(ui.launcher, state.launcherPosition);
applyStoredPosition(ui.panel, state.panelPosition);
} else {
clearInlinePosition(ui.launcher);
clearInlinePosition(ui.panel);
}
syncUi();
}
function unmountUi() {
if (ui.panel && ui.panel.isConnected) ui.panel.remove();
if (ui.launcher && ui.launcher.isConnected) ui.launcher.remove();
var slot = document.getElementById(CONFIG.ids.uiSlot);
if (slot && !slot.childNodes.length) slot.remove();
}
function ensurePageControlsSlot() {
var host = getPageControlsHost();
if (!host) return null;
var slot = document.getElementById(CONFIG.ids.uiSlot);
if (!slot) {
slot = document.createElement('div');
slot.id = CONFIG.ids.uiSlot;
slot.className = 'yds-page-slot';
}
if (slot.parentNode !== host) {
host.appendChild(slot);
}
return slot;
}
function getPageControlsHost() {
return document.querySelector(CONFIG.selectors.metadataActionButtons) ||
document.querySelector(CONFIG.selectors.metadataActions) ||
document.querySelector(CONFIG.selectors.metadataTopRow);
}
function uiMountedInBestHost() {
if (!ui.launcher || !ui.launcher.isConnected) return false;
var host = getPageControlsHost();
if (!host) return true;
var slot = document.getElementById(CONFIG.ids.uiSlot);
return !!slot && slot.parentNode === host && ui.launcher.parentNode === slot;
}
function syncUi() {
syncTargetLanguageOptions();
syncSourceTrackOptions();
if (ui.displayMode) ui.displayMode.value = normalizeDisplayMode(state.displayMode);
if (ui.targetLang) ui.targetLang.value = state.targetLang;
if (ui.trackIndex) ui.trackIndex.value = String(state.sourceTrackIndex);
if (ui.sourceName) ui.sourceName.textContent = app.lastSourceName || TEXT.unselected;
if (ui.status) ui.status.textContent = app.status;
if (ui.debugToggle) ui.debugToggle.checked = !!state.debug;
if (ui.enabledBtn) ui.enabledBtn.textContent = state.enabled ? TEXT.disableDualSubs : TEXT.enableDualSubs;
syncStyleControls();
applySubtitleStyle();
if (ui.debugBox) {
ui.debugBox.hidden = !isDebugEnabled(state);
ui.debugBox.textContent = formatDebugText();
}
}
function syncTargetLanguageOptions(force) {
if (!ui.targetLang) return;
var rawOptions = (app.translationLanguages || []).slice();
var query = ui.targetSearch ? normalizeSearchText(ui.targetSearch.value) : '';
var options = query ? rawOptions.filter(function (option) {
return normalizeSearchText(option.languageCode + ' ' + option.name).indexOf(query) !== -1;
}) : rawOptions;
var hasCurrent = false;
var i;
for (i = 0; i < options.length; i++) {
if (options[i].languageCode === state.targetLang) {
hasCurrent = true;
break;
}
}
if (!hasCurrent) {
options.unshift({
languageCode: state.targetLang,
name: state.targetLang
});
}
var signature = options.map(function (option) {
return option.languageCode + ':' + option.name;
}).join('|') + '|q=' + query;
if (!force && ui.targetLang.getAttribute('data-options-signature') === signature) return;
ui.targetLang.textContent = '';
options.forEach(function (option) {
var node = document.createElement('option');
node.value = option.languageCode;
node.textContent = option.name + ' (' + option.languageCode + ')';
ui.targetLang.appendChild(node);
});
ui.targetLang.setAttribute('data-options-signature', signature);
}
function syncSourceTrackOptions() {
if (!ui.trackIndex) return;
var tracks = app.tracks || [];
var options = [];
var i;
for (i = 0; i < tracks.length; i++) {
options.push({
index: i,
label: formatTrackLabel(tracks[i], i)
});
}
if (!options.length) {
options.push({
index: state.sourceTrackIndex,
label: '#' + state.sourceTrackIndex
});
}
var signature = options.map(function (option) {
return option.index + ':' + option.label;
}).join('|');
if (ui.trackIndex.getAttribute('data-options-signature') === signature) return;
ui.trackIndex.textContent = '';
options.forEach(function (option) {
var node = document.createElement('option');
node.value = String(option.index);
node.textContent = option.label;
ui.trackIndex.appendChild(node);
});
ui.trackIndex.setAttribute('data-options-signature', signature);
}
function getCurrentSourceTrack() {
return app.tracks && app.tracks[state.sourceTrackIndex] ? app.tracks[state.sourceTrackIndex] : null;
}
function getSourceMemoryKey(track) {
if (!track || !track.languageCode) return '';
return String(track.languageCode || '').trim();
}
function rememberTargetForCurrentSource() {
rememberTargetForTrack(getCurrentSourceTrack(), state.targetLang);
}
function rememberTargetForTrack(track, targetLang) {
var key = getSourceMemoryKey(track);
if (!key || !targetLang) return false;
if (!state.targetLangBySource || typeof state.targetLangBySource !== 'object') state.targetLangBySource = {};
state.targetLangBySource[key] = String(targetLang);
return true;
}
function applyRememberedTargetForTrack(track, fallbackToDefault) {
var key = getSourceMemoryKey(track);
var remembered = key && state.targetLangBySource ? state.targetLangBySource[key] : '';
if (!remembered && fallbackToDefault) remembered = DEFAULTS.targetLang;
if (!remembered || remembered === state.targetLang) return false;
state.targetLang = remembered;
return true;
}
function syncStyleControls() {
syncNumberRange(ui.sourceFontSize, state.sourceFontSize);
syncNumberRange(ui.targetFontSize, state.targetFontSize);
syncNumberRange(ui.lineGap, state.lineGap);
syncNumberRange(ui.bottomOffset, state.bottomOffset);
if (ui.fontFamily) ui.fontFamily.value = normalizeFontFamily(state.fontFamily);
syncColor(ui.sourceColor, state.sourceColor);
syncColor(ui.targetColor, state.targetColor);
}
function syncNumberRange(control, value) {
if (!control) return;
control.range.value = String(value);
control.number.value = String(value);
}
function syncColor(control, value) {
if (!control) return;
control.input.value = normalizeColor(value, '#ffffff');
syncColorPreview(control.preview, control.input.value);
}
function syncColorPreview(node, value) {
if (!node) return;
node.style.color = normalizeColor(value, '#ffffff');
}
function resetSubtitleStyle() {
state.sourceFontSize = DEFAULTS.sourceFontSize;
state.targetFontSize = DEFAULTS.targetFontSize;
state.lineGap = DEFAULTS.lineGap;
state.bottomOffset = DEFAULTS.bottomOffset;
state.sourceColor = DEFAULTS.sourceColor;
state.targetColor = DEFAULTS.targetColor;
state.fontFamily = DEFAULTS.fontFamily;
saveSettings(state);
syncUi();
}
function setDetachedClass(node, detached) {
if (!node) return;
node.classList.toggle('yds-detached', !!detached);
}
function clearInlinePosition(node) {
if (!node) return;
node.style.left = '';
node.style.top = '';
node.style.right = '';
node.style.bottom = '';
}
function applyStoredPosition(node, position) {
if (!node) return;
if (!position || typeof position.left !== 'number' || typeof position.top !== 'number') {
node.style.left = '';
node.style.top = '';
node.style.right = '';
node.style.bottom = '';
return;
}
node.style.left = position.left + 'px';
node.style.top = position.top + 'px';
node.style.right = 'auto';
node.style.bottom = 'auto';
}
function enableDragging(node, handle, stateKey) {
if (!node || !handle) return;
var drag = null;
function onPointerMove(event) {
if (!drag) return;
var nextLeft = clampToViewport(drag.startLeft + (event.clientX - drag.startX), node.offsetWidth, window.innerWidth);
var nextTop = clampToViewport(drag.startTop + (event.clientY - drag.startY), node.offsetHeight, window.innerHeight);
applyStoredPosition(node, { left: nextLeft, top: nextTop });
}
function onPointerUp() {
if (!drag) return;
state[stateKey] = {
left: parseFloat(node.style.left) || 0,
top: parseFloat(node.style.top) || 0
};
saveSettings(state);
drag = null;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
}
handle.addEventListener('pointerdown', function (event) {
if (event.button != null && event.button !== 0) return;
if (isInteractiveTarget(event.target)) return;
var rect = node.getBoundingClientRect();
drag = {
startX: event.clientX,
startY: event.clientY,
startLeft: rect.left,
startTop: rect.top
};
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerUp);
event.preventDefault();
});
app.teardown.push(function () {
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
});
}
function setPhase(phase) {
app.phase = phase;
syncUi();
}
function setStatus(text) {
app.status = String(text || '');
syncUi();
logger.debug('status', { phase: app.phase, text: app.status });
}
function setDualSubsEnabled(enabled, reason) {
state.enabled = !!enabled;
saveSettings(state);
if (state.enabled) {
reloadDualSubsSoon(reason || 'enabled');
return;
}
app.pendingLoadKey = '';
app.backoffUntil = 0;
app.loading = false;
app.cuesA = [];
app.cuesB = [];
stopLoop();
clearNativeCaptionWindow();
setPhase('disabled');
setStatus(TEXT.disabled);
syncUi();
}
function reloadDualSubsSoon(reason) {
if (!state.enabled) {
setDualSubsEnabled(false, reason || 'setting-change-disabled');
return;
}
app.pendingLoadKey = '';
app.backoffUntil = 0;
app.cuesA = [];
app.cuesB = [];
stopLoop();
clearNativeCaptionWindow();
setPhase('load-start');
setStatus(TEXT.loading);
if (!isWatchPage() || !getVideo() || !getPlayer()) {
scheduleInit(reason || 'setting-change', 0);
return;
}
loadDualSubs(true, reason || 'setting-change');
}
function formatDebugText() {
var snapshot = collectSnapshot();
return [
'version=' + snapshot.version,
'page=' + snapshot.pageType,
'url=' + snapshot.url,
'videoId=' + (snapshot.videoId || '-'),
'phase=' + snapshot.phase,
'loading=' + snapshot.loading,
'enabled=' + snapshot.enabled,
'display-mode=' + snapshot.displayMode,
'caption-source=' + (snapshot.captionSource || '-'),
'default-track=' + snapshot.defaultTrackIndex,
'source=' + (snapshot.source || '-'),
'tracks=' + snapshot.tracks.length,
'cues=' + snapshot.cuesA + '/' + snapshot.cuesB,
'transcript-trigger=' + (snapshot.transcriptTrigger || '-'),
'fallback=' + (snapshot.fallback || '-'),
'fetch-source=' + (snapshot.fetch.source || '-'),
'fetch-target=' + (snapshot.fetch.target || '-'),
'dom=video:' + snapshot.dom.video + ',player:' + snapshot.dom.player + ',captions:' + snapshot.dom.captionContainer,
'injected=launcher:' + snapshot.dom.launcher + ',panel:' + snapshot.dom.panel + ',native:' + snapshot.dom.nativeWindow
].join('\n');
}
function bindGlobalListeners() {
ensureHistoryHook();
addWindowListener('yt-navigate-finish', function () {
handleNavigation('yt-navigate-finish');
});
addWindowListener('yt-page-data-updated', function () {
handleNavigation('yt-page-data-updated');
});
addWindowListener('ytp-history-navigate', function () {
handleNavigation('ytp-history-navigate');
});
addWindowListener(CONFIG.historyEventName, function () {
handleNavigation(CONFIG.historyEventName);
});
addWindowListener('popstate', function () {
handleNavigation('popstate');
});
addWindowListener('load', function () {
handleNavigation('window-load');
});
app.timers.urlPoll = window.setInterval(function () {
if (location.href !== app.lastUrl) {
handleNavigation('url-poll');
return;
}
if (isWatchPage() && (!getVideo() || !getPlayer() || !uiMountedInBestHost())) {
scheduleInit('url-poll-health', CONFIG.domDebounceMs);
}
}, CONFIG.routePollMs);
var observationTarget = getObservationTarget();
if (typeof MutationObserver === 'function' && observationTarget) {
app.observer = new MutationObserver(function (mutations) {
if (!isWatchPage()) return;
if (!hasRelevantMutation(mutations)) return;
scheduleInit('dom-mutation', CONFIG.domDebounceMs);
});
app.observer.observe(observationTarget, {
childList: true,
subtree: observationTarget !== document.body && observationTarget !== document.documentElement
});
}
}
function addWindowListener(type, handler) {
window.addEventListener(type, handler);
app.teardown.push(function () {
window.removeEventListener(type, handler);
});
}
function handleNavigation(reason) {
logger.debug('navigation', { reason: reason, href: location.href });
scheduleInit(reason, CONFIG.initDelayMs);
}
function scheduleInit(reason, delayMs) {
clearTimeout(app.timers.init);
app.timers.init = window.setTimeout(function () {
initForPage(reason);
}, typeof delayMs === 'number' ? delayMs : CONFIG.initDelayMs);
}
function initForPage(reason) {
app.lastUrl = location.href;
mountUi();
exposeDebugApi();
if (!isWatchPage()) {
resetForNonWatch();
return;
}
var videoId = getVideoId();
if (videoId !== app.lastVideoId) {
resetVideoState(videoId, reason);
}
if (!getVideo() || !getPlayer()) {
setPhase('wait-player');
setStatus(TEXT.waitingPlayer);
scheduleInit('wait-player', CONFIG.retryDelayMs);
return;
}
loadDualSubs(false, reason || 'init');
}
function resetForNonWatch() {
app.activeRequestId += 1;
app.loading = false;
app.defaultTrackIndex = -1;
app.lastCaptionSource = '';
app.lastFallback = '';
app.lastVideoId = '';
app.lastSourceName = '';
app.cuesA = [];
app.cuesB = [];
clearFetchDiagnostics();
app.tracks = [];
app.trackRetryCount = 0;
app.pendingLoadKey = '';
app.translationLanguages = [];
setPhase('idle');
setStatus(TEXT.waitingWatchPage);
stopLoop();
clearNativeCaptionWindow();
}
function resetVideoState(videoId, reason) {
logger.debug('reset video state', {
from: app.lastVideoId || '',
to: videoId || '',
reason: reason || 'unknown'
});
app.activeRequestId += 1;
app.loading = false;
app.defaultTrackIndex = -1;
app.lastCaptionSource = '';
app.lastFallback = '';
app.lastVideoId = videoId || '';
app.lastSourceName = '';
app.cuesA = [];
app.cuesB = [];
clearFetchDiagnostics();
app.tracks = [];
app.trackRetryCount = 0;
app.pendingLoadKey = '';
app.translationLanguages = [];
stopLoop();
clearNativeCaptionWindow();
syncUi();
}
function loadDualSubs(force, reason) {
if (!isWatchPage()) return;
if (!state.enabled) {
app.loading = false;
app.pendingLoadKey = '';
app.cuesA = [];
app.cuesB = [];
stopLoop();
clearNativeCaptionWindow();
setPhase('disabled');
setStatus(TEXT.disabled);
return;
}
var videoId = getVideoId();
var loadKey = [videoId, state.targetLang, state.sourceTrackIndex].join('|');
if (!force && !app.loading && app.pendingLoadKey === loadKey && (app.cuesA.length || app.cuesB.length)) {
logger.debug('skip completed load', { reason: reason, loadKey: loadKey });
if (!app.loopId) startLoop();
return;
}
if (!force && app.loading && app.pendingLoadKey === loadKey) {
logger.debug('skip duplicate load', { reason: reason, loadKey: loadKey });
return;
}
var now = Date.now();
if (!force && app.backoffUntil && now < app.backoffUntil) {
setPhase('backoff');
setStatus(TEXT.rateLimitedShort);
return;
}
app.pendingLoadKey = loadKey;
app.loading = true;
app.activeRequestId += 1;
var requestId = app.activeRequestId;
clearFetchDiagnostics();
app.lastFallback = '';
ensureCaptionsEnabled();
setPhase('load-start');
setStatus(TEXT.loading);
getBestCaptionData(videoId).then(function (captionData) {
if (!isActiveRequest(requestId, videoId)) return;
var tracks = captionData.tracks || [];
app.tracks = tracks;
app.defaultTrackIndex = typeof captionData.defaultTrackIndex === 'number' ? captionData.defaultTrackIndex : -1;
app.lastCaptionSource = captionData.source || '';
app.lastFallback = '';
app.translationLanguages = getTranslationLanguages(captionData.playerResponse, tracks, null);
logger.debug('caption tracks resolved', {
loadKey: loadKey,
source: captionData.source,
tracks: tracks.length
});
var selected = chooseTrack(tracks, state.sourceTrackIndex, state.targetLang);
if (!selected.track) {
app.loading = false;
app.cuesA = [];
app.cuesB = [];
app.lastSourceName = TEXT.noTrack;
stopLoop();
if (!tracks.length && app.trackRetryCount < CONFIG.maxTrackRetries) {
app.trackRetryCount += 1;
setPhase('wait-tracks');
setStatus(TEXT.waitingTracks + ' (' + app.trackRetryCount + '/' + CONFIG.maxTrackRetries + ')');
scheduleInit('wait-tracks', CONFIG.retryDelayMs);
} else {
setPhase('no-tracks');
setStatus(TEXT.noTrackDetail);
}
syncUi();
return null;
}
app.trackRetryCount = 0;
applyRememberedTargetForTrack(selected.track);
state.sourceTrackIndex = selected.index;
rememberTargetForTrack(selected.track, state.targetLang);
saveSettings(state);
app.pendingLoadKey = [videoId, state.targetLang, state.sourceTrackIndex].join('|');
app.lastSourceName = formatTrackLabel(selected.track, selected.index);
app.translationLanguages = getTranslationLanguages(captionData.playerResponse, tracks, selected.track);
syncUi();
logger.debug('track pair', {
sourceLang: selected.track.languageCode || '',
targetLang: state.targetLang,
targetMode: 'translated'
});
return fetchBestPair(selected.track, null, state.targetLang).then(function (result) {
if (!isActiveRequest(requestId, videoId)) return;
if (result.fallback) {
app.lastFallback = app.lastFallback ? app.lastFallback + ' | ' + result.fallback : result.fallback;
}
if (!result.cuesA.length && !result.cuesB.length && canUseDefaultTrackFallback(selected.track, tracks[captionData.defaultTrackIndex], captionData.defaultTrackIndex, selected.index)) {
var fallbackTrack = tracks[captionData.defaultTrackIndex];
app.lastFallback = 'default-track:' + selected.index + '->' + captionData.defaultTrackIndex;
logger.debug('retry with default caption track', {
from: selected.index,
to: captionData.defaultTrackIndex,
targetLang: state.targetLang
});
return fetchBestPair(fallbackTrack, null, state.targetLang).then(function (fallbackResult) {
if (!isActiveRequest(requestId, videoId)) return;
if (fallbackResult.fallback) {
app.lastFallback = app.lastFallback ? app.lastFallback + ' | ' + fallbackResult.fallback : fallbackResult.fallback;
}
if (fallbackResult.cuesA.length || fallbackResult.cuesB.length) {
app.lastSourceName = formatTrackLabel(fallbackTrack, captionData.defaultTrackIndex) + ' [fallback]';
syncUi();
return applyLoadedCues(fallbackResult);
}
app.lastFallback = app.lastFallback + ':empty';
return applyLoadedCues(result);
});
}
return applyLoadedCues(result);
});
}).catch(function (err) {
if (!isActiveRequest(requestId, videoId)) return;
app.cuesA = [];
app.cuesB = [];
stopLoop();
if (err && err.status === 429) {
app.backoffUntil = Date.now() + CONFIG.rateLimitBackoffMs;
setPhase('backoff');
setStatus(TEXT.rateLimited);
} else {
setPhase('load-error');
setStatus(TEXT.loadFailed + formatError(err));
}
logger.error('loadDualSubs failed', err);
}).finally(function () {
if (!isRequestCurrent(requestId)) return;
app.loading = false;
syncUi();
});
function applyLoadedCues(result) {
if (!isActiveRequest(requestId, videoId)) return;
app.backoffUntil = 0;
app.cuesA = result.cuesA || [];
app.cuesB = result.cuesB || [];
if (!app.cuesA.length && !app.cuesB.length) {
setPhase('no-cues');
setStatus(TEXT.noCue);
stopLoop();
return;
}
setPhase('ready');
if (!app.cuesB.length) {
setStatus(TEXT.sourceOnly);
} else {
setStatus(TEXT.nativeReady);
}
startLoop();
}
}
function isRequestCurrent(requestId) {
return requestId === app.activeRequestId;
}
function isActiveRequest(requestId, videoId) {
return isRequestCurrent(requestId) && videoId === getVideoId() && isWatchPage();
}
function startLoop() {
stopLoop();
function tick() {
if (!isWatchPage()) {
stopLoop();
return;
}
var video = getVideo();
if (!video) {
app.loopId = requestAnimationFrame(tick);
return;
}
renderCurrentCaption();
app.loopId = requestAnimationFrame(tick);
}
app.loopId = requestAnimationFrame(tick);
}
function renderCurrentCaption() {
if (!state.enabled) {
clearNativeCaptionWindow();
return;
}
var video = getVideo();
if (!video) return;
var mode = normalizeDisplayMode(state.displayMode);
var textA = mode === 'target' ? '' : findCueText(app.cuesA, video.currentTime);
var textB = mode === 'source' ? '' : findCueText(app.cuesB, video.currentTime);
renderNativeCaption(textA, textB);
}
function stopLoop() {
if (app.loopId) cancelAnimationFrame(app.loopId);
app.loopId = 0;
clearNativeCaptionWindow();
}
function exposeDebugApi() {
var pageWindow = getPageWindow();
pageWindow[DEBUG_API_KEY] = {
runtime: api,
reload: function () {
reloadDualSubsSoon('debug-reload');
},
scheduleInit: function (reason) {
scheduleInit(reason || 'debug', 0);
},
setDebug: function (enabled) {
state.debug = !!enabled;
saveSettings(state);
syncUi();
return collectSnapshot();
},
setEnabled: function (enabled) {
setDualSubsEnabled(!!enabled, 'debug-set-enabled');
return collectSnapshot();
},
snapshot: function () {
return collectSnapshot();
}
};
}
function collectSnapshot() {
var tracks = [];
var i;
for (i = 0; i < app.tracks.length; i++) {
tracks.push({
index: i,
lang: app.tracks[i].languageCode || '',
name: getTrackName(app.tracks[i]),
hasBaseUrl: !!app.tracks[i].baseUrl
});
}
return {
captionSource: app.lastCaptionSource,
cuesA: app.cuesA.length,
cuesB: app.cuesB.length,
defaultTrackIndex: app.defaultTrackIndex,
dom: {
captionContainer: !!getCaptionContainer(),
launcher: !!document.getElementById(CONFIG.ids.launcher),
nativeWindow: !!document.getElementById(CONFIG.ids.nativeWindow),
panel: !!document.getElementById(CONFIG.ids.panel),
player: !!getPlayer(),
video: !!getVideo()
},
fetch: {
source: fetchDiagnostics.source,
target: fetchDiagnostics.target
},
loading: app.loading,
fallback: app.lastFallback,
nativeTimedTextHint: describeNativeTimedTextHint(),
pageType: isWatchPage() ? 'watch' : 'other',
phase: app.phase,
source: app.lastSourceName,
status: app.status,
enabled: state.enabled,
displayMode: state.displayMode,
smartPosition: state.smartPosition,
syncNativeStyle: state.syncNativeStyle,
transcriptTrigger: describeTranscriptTrigger(),
tracks: tracks,
targetLang: state.targetLang,
translationLanguages: app.translationLanguages,
url: location.href,
version: SCRIPT_VERSION,
videoId: getVideoId()
};
}
var api = {
boot: boot,
destroy: destroy,
snapshot: collectSnapshot
};
return api;
}
function loadSettings() {
var stored = GM_getValue(SETTINGS_KEY, {});
var merged = {};
var key;
for (key in DEFAULTS) merged[key] = DEFAULTS[key];
if (stored && typeof stored === 'object') {
for (key in stored) merged[key] = stored[key];
}
return normalizeSettings(merged);
}
function saveSettings(nextState) {
var normalized = normalizeSettings(nextState);
GM_setValue(SETTINGS_KEY, {
debug: !!normalized.debug,
enabled: !!normalized.enabled,
panelOpen: false,
launcherPosition: normalizePosition(normalized.launcherPosition),
panelPosition: normalizePosition(normalized.panelPosition),
sourceTrackIndex: normalized.sourceTrackIndex,
targetLang: normalized.targetLang,
targetLangBySource: normalized.targetLangBySource,
displayMode: normalized.displayMode,
sourceFontSize: normalized.sourceFontSize,
targetFontSize: normalized.targetFontSize,
lineGap: normalized.lineGap,
bottomOffset: normalized.bottomOffset,
sourceColor: normalized.sourceColor,
targetColor: normalized.targetColor,
fontFamily: normalized.fontFamily,
smartPosition: true,
syncNativeStyle: true
});
}
function normalizeSettings(input) {
var output = {};
var key;
input = input || {};
for (key in DEFAULTS) output[key] = DEFAULTS[key];
for (key in input) output[key] = input[key];
output.debug = !!output.debug;
output.enabled = output.enabled !== false;
output.panelOpen = false;
output.launcherPosition = normalizePosition(output.launcherPosition);
output.panelPosition = normalizePosition(output.panelPosition);
output.sourceTrackIndex = Math.max(0, parseInt(output.sourceTrackIndex || '0', 10) || 0);
output.targetLang = String(output.targetLang || DEFAULTS.targetLang).trim() || DEFAULTS.targetLang;
output.targetLangBySource = normalizeTargetLangBySource(output.targetLangBySource);
output.displayMode = normalizeDisplayMode(output.displayMode);
output.sourceFontSize = clampNumber(parseFloat(output.sourceFontSize), 16, 56, DEFAULTS.sourceFontSize);
output.targetFontSize = clampNumber(parseFloat(output.targetFontSize), 16, 56, DEFAULTS.targetFontSize);
output.lineGap = clampNumber(parseFloat(output.lineGap), 0, 24, DEFAULTS.lineGap);
output.bottomOffset = clampNumber(parseFloat(output.bottomOffset), 2, 28, DEFAULTS.bottomOffset);
output.sourceColor = normalizeColor(output.sourceColor, DEFAULTS.sourceColor);
output.targetColor = normalizeColor(output.targetColor, DEFAULTS.targetColor);
output.fontFamily = normalizeFontFamily(output.fontFamily);
output.smartPosition = true;
output.syncNativeStyle = true;
return output;
}
function normalizeTargetLangBySource(value) {
var output = {};
var key;
if (!value || typeof value !== 'object') return output;
for (key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
var source = String(key || '').trim();
var target = String(value[key] || '').trim();
if (source && target) output[source] = target;
}
return output;
}
function normalizePosition(position) {
if (!position || typeof position.left !== 'number' || typeof position.top !== 'number') return null;
return {
left: Math.max(0, Math.round(position.left)),
top: Math.max(0, Math.round(position.top))
};
}
function clampNumber(value, min, max, fallback) {
if (!isFinite(value)) return fallback;
if (value < min) return min;
if (value > max) return max;
return Math.round(value);
}
function normalizeColor(value, fallback) {
var text = String(value || '').trim();
if (/^#[0-9a-f]{6}$/i.test(text)) return text.toLowerCase();
return fallback;
}
function normalizeSearchText(value) {
return String(value || '').toLowerCase().replace(/\s+/g, ' ').trim();
}
function normalizeFontFamily(value) {
var text = String(value || '').trim();
var i;
for (i = 0; i < FONT_OPTIONS.length; i++) {
if (FONT_OPTIONS[i].value === text) return text;
}
return DEFAULTS.fontFamily;
}
function normalizeDisplayMode(value) {
var text = String(value || '').trim();
if (text === 'source' || text === 'target' || text === 'dual') return text;
return DEFAULTS.displayMode;
}
function getFontCss(value) {
var normalized = normalizeFontFamily(value);
var i;
for (i = 0; i < FONT_OPTIONS.length; i++) {
if (FONT_OPTIONS[i].value === normalized) return FONT_OPTIONS[i].css;
}
return FONT_OPTIONS[0].css;
}
function clearFetchDiagnostics() {
fetchDiagnostics.source = '';
fetchDiagnostics.target = '';
}
function setFetchDiagnostic(label, value) {
fetchDiagnostics[label] = value;
}
function appendFetchDiagnostic(label, value) {
fetchDiagnostics[label] = fetchDiagnostics[label] ? fetchDiagnostics[label] + ' | ' + value : value;
}
function createLogger(isEnabled) {
function emit(level, message, meta) {
var fn = console[level] || console.log;
if (typeof meta === 'undefined') {
fn.call(console, LOG_PREFIX + ' ' + message);
} else {
fn.call(console, LOG_PREFIX + ' ' + message, meta);
}
}
return {
debug: function (message, meta) {
if (!isEnabled()) return;
emit('debug', message, meta);
},
error: function (message, meta) {
emit('error', message, meta);
}
};
}
function isDebugEnabled(currentState) {
return !!currentState.debug || location.search.indexOf(CONFIG.debugQueryParam) !== -1;
}
function isWatchPage() {
return location.pathname === CONFIG.selectors.watchPath;
}
function getRoot() {
return document.body || document.documentElement;
}
function getPageWindow() {
return typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
}
function getPageFetch() {
var pageWindow = getPageWindow();
if (pageWindow && typeof pageWindow.fetch === 'function') {
return pageWindow.fetch.bind(pageWindow);
}
if (typeof fetch === 'function') {
return fetch.bind(window);
}
return null;
}
function installTimedTextObserver() {
var pageWindow = getPageWindow();
var observerKey = '__ydsTimedTextObserver';
if (!pageWindow || pageWindow[observerKey]) return;
pageWindow[observerKey] = {
installedAt: Date.now()
};
try {
if (typeof pageWindow.fetch === 'function') {
var originalFetch = pageWindow.fetch;
pageWindow.fetch = function () {
rememberRequestTimedTextUrl(arguments[0]);
return originalFetch.apply(this, arguments);
};
}
} catch (err) {
logger.error('fetch observer install failed', err);
}
try {
if (pageWindow.XMLHttpRequest && pageWindow.XMLHttpRequest.prototype) {
var originalOpen = pageWindow.XMLHttpRequest.prototype.open;
pageWindow.XMLHttpRequest.prototype.open = function (method, url) {
rememberRequestTimedTextUrl(url);
return originalOpen.apply(this, arguments);
};
}
} catch (xhrErr) {
logger.error('xhr observer install failed', xhrErr);
}
}
function rememberRequestTimedTextUrl(input) {
try {
if (!input) return;
var url = typeof input === 'string' ? input : (input.url || String(input));
rememberNativeTimedTextUrl(url);
} catch (err) {
logger.debug('remember timedtext request failed', err);
}
}
function collectNativeTimedTextHints() {
try {
if (!window.performance || typeof window.performance.getEntriesByType !== 'function') return;
var entries = window.performance.getEntriesByType('resource') || [];
var start = Math.max(0, entries.length - 80);
var i;
for (i = start; i < entries.length; i++) {
rememberNativeTimedTextUrl(entries[i] && entries[i].name);
}
} catch (err) {
logger.debug('performance timedtext scan failed', err);
}
}
function rememberNativeTimedTextUrl(rawUrl) {
if (!rawUrl || String(rawUrl).indexOf('/api/timedtext') === -1) return;
var parsed;
try {
parsed = new URL(rawUrl, location.href);
} catch (err) {
return;
}
if (parsed.hostname !== 'www.youtube.com' && parsed.hostname !== 'youtube.com') return;
var videoId = parsed.searchParams.get('v') || '';
if (!videoId) return;
var params = extractNativeTimedTextParams(parsed);
if (!params) return;
resetNativeTimedTextHints(videoId);
var hint = {
lang: parsed.searchParams.get('lang') || '',
params: params,
source: 'native-request',
updatedAt: Date.now()
};
nativeTimedTextHints.last = hint;
if (hint.lang) nativeTimedTextHints.byLang[hint.lang] = hint;
}
function resetNativeTimedTextHints(videoId) {
if (nativeTimedTextHints.videoId === videoId) return;
nativeTimedTextHints.videoId = videoId;
nativeTimedTextHints.byLang = {};
nativeTimedTextHints.last = null;
}
function extractNativeTimedTextParams(url) {
var params = {};
var hasHint = false;
var i;
for (i = 0; i < CONFIG.nativeTimedTextParamKeys.length; i++) {
var key = CONFIG.nativeTimedTextParamKeys[i];
if (!url.searchParams.has(key)) continue;
params[key] = url.searchParams.get(key);
hasHint = true;
}
if (!hasHint || !params.pot) return null;
return params;
}
function describeNativeTimedTextHint() {
var hint = getNativeTimedTextHint(null);
if (!hint || !hint.params) return null;
var keys = [];
var key;
for (key in hint.params) keys.push(key);
return {
ageMs: Math.max(0, Date.now() - hint.updatedAt),
keys: keys,
lang: hint.lang || '',
videoId: nativeTimedTextHints.videoId || ''
};
}
function getNativeTimedTextHint(track) {
collectNativeTimedTextHints();
var currentVideoId = getVideoId();
if (currentVideoId && nativeTimedTextHints.videoId && nativeTimedTextHints.videoId !== currentVideoId) {
resetNativeTimedTextHints(currentVideoId);
}
if (!track) return nativeTimedTextHints.last;
var languageCode = track.languageCode || '';
if (languageCode && nativeTimedTextHints.byLang[languageCode]) return nativeTimedTextHints.byLang[languageCode];
var prefix = languageCode ? String(languageCode).split('-')[0] : '';
var key;
if (prefix) {
for (key in nativeTimedTextHints.byLang) {
if (String(key || '').split('-')[0] === prefix) return nativeTimedTextHints.byLang[key];
}
}
return nativeTimedTextHints.last;
}
function waitForNativeTimedTextHint(track) {
if (getNativeTimedTextHint(track)) return Promise.resolve(true);
if (window.__ydsHarnessShimInstalled) return Promise.resolve(false);
return waitFor(function () {
return getNativeTimedTextHint(track);
}, CONFIG.nativeTimedTextHintWaitMs, 120).then(function (hint) {
return !!hint;
});
}
function getBrowserLikeUserAgent() {
try {
return navigator && navigator.userAgent ? navigator.userAgent : 'Mozilla/5.0';
} catch (err) {
return 'Mozilla/5.0';
}
}
function buildInnertubeContext() {
return {
client: {
clientName: 'WEB',
clientVersion: getInnertubeClientVersion(),
hl: document.documentElement && document.documentElement.lang ? document.documentElement.lang : 'zh-CN',
visitorData: getInnertubeVisitorData()
}
};
}
function buildInnertubeHeaders() {
var headers = {
'Content-Type': 'application/json',
'X-YouTube-Client-Name': '1',
'X-YouTube-Client-Version': getInnertubeClientVersion()
};
var visitorData = getInnertubeVisitorData();
if (visitorData) headers['X-Goog-Visitor-Id'] = visitorData;
return headers;
}
function getVideoId() {
return new URLSearchParams(location.search).get('v') || '';
}
function getVideo() {
return document.querySelector(CONFIG.selectors.rootVideo);
}
function getPlayer() {
return document.querySelector(CONFIG.selectors.player);
}
function getCaptionContainer() {
return document.querySelector(CONFIG.selectors.captionContainer);
}
function getObservationTarget() {
return document.querySelector('#content') || document.querySelector('#page-manager') || document.body || document.documentElement;
}
function ensureHistoryHook() {
if (window.__ydsHistoryHookInstalled) return;
window.__ydsHistoryHookInstalled = true;
function dispatchHistoryChange(source) {
try {
window.dispatchEvent(new CustomEvent(CONFIG.historyEventName, {
detail: {
href: location.href,
source: source
}
}));
} catch (err) {
logger.error('history hook dispatch failed', err);
}
}
function patchHistoryMethod(name) {
if (!history || typeof history[name] !== 'function') return;
var original = history[name];
history[name] = function () {
var result = original.apply(this, arguments);
window.setTimeout(function () {
dispatchHistoryChange(name);
}, 0);
return result;
};
}
patchHistoryMethod('pushState');
patchHistoryMethod('replaceState');
}
function ensureCaptionsEnabled() {
var btn = document.querySelector(CONFIG.selectors.subtitlesButton);
if (!btn) return;
if (btn.getAttribute('aria-pressed') === 'false') btn.click();
}
function clearNativeCaptionWindow() {
var container = getCaptionContainer();
if (container) container.classList.remove('yds-native-mode');
var node = document.getElementById(CONFIG.ids.nativeWindow);
if (node) node.remove();
}
function ensureNativeCaptionWindow() {
var container = getCaptionContainer();
var player = getPlayer();
if (!player) return null;
if (container) container.classList.add('yds-native-mode');
var node = document.getElementById(CONFIG.ids.nativeWindow);
if (!node) {
node = document.createElement('div');
node.id = CONFIG.ids.nativeWindow;
node.className = 'yds-native-window';
var lineA = document.createElement('div');
lineA.className = 'yds-native-line yds-native-line-a';
var lineB = document.createElement('div');
lineB.className = 'yds-native-line yds-native-line-b';
node.appendChild(lineA);
node.appendChild(lineB);
player.appendChild(node);
} else if (node.parentNode !== player) {
player.appendChild(node);
}
applySubtitleStyle(node);
return node;
}
function applySubtitleStyle(node) {
node = node || document.getElementById(CONFIG.ids.nativeWindow);
if (!node) return;
var lineA = node.querySelector('.yds-native-line-a');
var lineB = node.querySelector('.yds-native-line-b');
var nativeStyle = state.syncNativeStyle ? getNativeCaptionStyleHint() : null;
var useNativeFont = nativeStyle && nativeStyle.fontFamily && normalizeFontFamily(state.fontFamily) === DEFAULTS.fontFamily;
var useNativeSourceSize = nativeStyle && nativeStyle.fontSize && Number(state.sourceFontSize) === DEFAULTS.sourceFontSize;
var useNativeTargetSize = nativeStyle && nativeStyle.fontSize && Number(state.targetFontSize) === DEFAULTS.targetFontSize;
var useNativeSourceColor = nativeStyle && nativeStyle.color && normalizeColor(state.sourceColor, DEFAULTS.sourceColor) === DEFAULTS.sourceColor;
var fontCss = useNativeFont ? nativeStyle.fontFamily : getFontCss(state.fontFamily);
var sourceFontSize = useNativeSourceSize ? nativeStyle.fontSize : clampNumber(parseFloat(state.sourceFontSize), 16, 56, DEFAULTS.sourceFontSize) + 'px';
var targetFontSize = useNativeTargetSize ? nativeStyle.fontSize : clampNumber(parseFloat(state.targetFontSize), 16, 56, DEFAULTS.targetFontSize) + 'px';
node.style.bottom = getSubtitleBottomPercent() + '%';
if (lineA) {
lineA.style.fontFamily = fontCss;
lineA.style.fontSize = sourceFontSize;
lineA.style.fontWeight = nativeStyle && nativeStyle.fontWeight ? nativeStyle.fontWeight : '600';
lineA.style.color = useNativeSourceColor ? nativeStyle.color : normalizeColor(state.sourceColor, DEFAULTS.sourceColor);
applyNativeBackgroundStyle(lineA, nativeStyle);
}
if (lineB) {
lineB.style.fontFamily = fontCss;
lineB.style.fontSize = targetFontSize;
lineB.style.fontWeight = nativeStyle && nativeStyle.fontWeight ? nativeStyle.fontWeight : '600';
lineB.style.color = nativeStyle && nativeStyle.color && state.displayMode === 'target' ? nativeStyle.color : normalizeColor(state.targetColor, DEFAULTS.targetColor);
applyNativeBackgroundStyle(lineB, nativeStyle);
lineB.style.marginTop = lineA && lineA.style.display !== 'none' && lineB.style.display !== 'none'
? clampNumber(parseFloat(state.lineGap), 0, 24, DEFAULTS.lineGap) + 'px'
: '0';
}
}
function getSubtitleBottomPercent() {
var base = clampNumber(parseFloat(state.bottomOffset), 2, 28, DEFAULTS.bottomOffset);
var player = getPlayer();
var controls = player ? player.querySelector(CONFIG.selectors.playerControls) : null;
if (!player || !controls || !player.clientHeight) return base;
var style = window.getComputedStyle(controls);
var controlsVisible = !player.classList.contains('ytp-autohide') &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
controls.offsetHeight > 0;
if (!controlsVisible) return base;
var controlsPercent = Math.ceil((controls.offsetHeight / player.clientHeight) * 100) + 3;
return Math.max(base, Math.min(28, controlsPercent));
}
function getNativeCaptionStyleHint() {
var node = document.querySelector(CONFIG.selectors.nativeCaptionText);
if (!node) return null;
var style = window.getComputedStyle(node);
var fontSize = parseFloat(style.fontSize);
var hint = {};
if (fontSize >= 16 && fontSize <= 72) hint.fontSize = Math.round(fontSize) + 'px';
if (style.fontFamily) hint.fontFamily = style.fontFamily;
if (style.fontWeight) hint.fontWeight = style.fontWeight;
if (style.color && style.color !== 'rgba(0, 0, 0, 0)') hint.color = style.color;
if (isVisibleBackgroundColor(style.backgroundColor)) hint.backgroundColor = style.backgroundColor;
return hint;
}
function applyNativeBackgroundStyle(line, nativeStyle) {
if (nativeStyle && nativeStyle.backgroundColor) {
line.style.backgroundColor = nativeStyle.backgroundColor;
line.style.borderRadius = '2px';
line.style.padding = '0 4px';
} else {
line.style.backgroundColor = '';
line.style.borderRadius = '';
line.style.padding = '';
}
}
function isVisibleBackgroundColor(value) {
var text = String(value || '').trim();
if (!text || text === 'transparent') return false;
if (/rgba\([^)]*,\s*0\)$/i.test(text)) return false;
return true;
}
function renderNativeCaption(textA, textB) {
if (!textA && !textB) {
clearNativeCaptionWindow();
return;
}
var node = ensureNativeCaptionWindow();
if (!node) return;
var lineA = node.querySelector('.yds-native-line-a');
var lineB = node.querySelector('.yds-native-line-b');
if (!lineA || !lineB) return;
lineA.textContent = textA || '';
lineB.textContent = textB || '';
lineA.style.display = textA ? 'block' : 'none';
lineB.style.display = textB ? 'block' : 'none';
applySubtitleStyle(node);
}
function hasRelevantMutation(mutations) {
var selector = [
CONFIG.selectors.rootVideo,
CONFIG.selectors.player,
CONFIG.selectors.captionContainer,
CONFIG.selectors.metadataTopRow,
CONFIG.selectors.metadataActions,
'#' + CONFIG.ids.uiSlot,
'#' + CONFIG.ids.launcher,
'#' + CONFIG.ids.panel
].join(',');
var i;
for (i = 0; i < mutations.length; i++) {
if (mutationHasRelevantNode(mutations[i].addedNodes, selector)) return true;
if (mutationHasRelevantNode(mutations[i].removedNodes, selector)) return true;
}
return false;
}
function mutationHasRelevantNode(nodeList, selector) {
var i;
for (i = 0; i < nodeList.length; i++) {
var node = nodeList[i];
if (!node || node.nodeType !== 1) continue;
if (matchesSelector(node, selector)) return true;
if (typeof node.querySelector === 'function' && node.querySelector(selector)) return true;
}
return false;
}
function matchesSelector(node, selector) {
var matcher = node.matches || node.msMatchesSelector || node.webkitMatchesSelector;
return !!matcher && matcher.call(node, selector);
}
function isInteractiveTarget(target) {
if (!target || typeof target.closest !== 'function') return false;
return !!target.closest('button,input,textarea,select,a,label');
}
function clampToViewport(value, size, max) {
var safeMax = Math.max(0, (max || 0) - (size || 0));
if (value < 0) return 0;
if (value > safeMax) return safeMax;
return value;
}
function clampIndex(index, length) {
if (!length) return 0;
if (index < 0) return 0;
if (index >= length) return length - 1;
return index;
}
function buildTimedTextUrl(baseUrl, params) {
var url = new URL(baseUrl, location.href);
var key;
for (key in params) {
if (params[key] == null || params[key] === '') {
url.searchParams.delete(key);
} else {
url.searchParams.set(key, params[key]);
}
}
return url.toString();
}
function findCueText(cues, time) {
var left = 0;
var right = cues.length - 1;
while (left <= right) {
var mid = (left + right) >> 1;
var cue = cues[mid];
if (time < cue.start) {
right = mid - 1;
} else if (time > cue.end) {
left = mid + 1;
} else {
return cue.text;
}
}
return '';
}
function chooseTrack(tracks, preferredIndex, targetLang) {
if (!tracks.length) return { index: 0, track: null };
var index = clampIndex(preferredIndex, tracks.length);
var track = getUsableTrack(tracks[index]) ? tracks[index] : null;
if (!track) {
index = findTrackIndex(tracks, function (item) {
return getUsableTrack(item);
});
track = index === -1 ? null : tracks[index];
}
if (track && track.languageCode === targetLang) {
var altIndex = findTrackIndex(tracks, function (item, itemIndex) {
return itemIndex !== index && getUsableTrack(item) && item.languageCode !== targetLang;
});
if (altIndex !== -1) {
index = altIndex;
track = tracks[altIndex];
}
}
return {
index: index < 0 ? 0 : index,
track: track
};
}
function findTrackIndex(tracks, predicate) {
var i;
for (i = 0; i < tracks.length; i++) {
if (predicate(tracks[i], i)) return i;
}
return -1;
}
function findTrackByLanguage(tracks, languageCode, excludeIndex) {
if (!languageCode) return null;
var exactIndex = findTrackIndex(tracks, function (track, index) {
return index !== excludeIndex && getUsableTrack(track) && track.languageCode === languageCode;
});
if (exactIndex !== -1) return tracks[exactIndex];
var prefix = String(languageCode).split('-')[0];
var prefixIndex = findTrackIndex(tracks, function (track, index) {
return index !== excludeIndex && getUsableTrack(track) && String(track.languageCode || '').split('-')[0] === prefix;
});
return prefixIndex === -1 ? null : tracks[prefixIndex];
}
function canUseDefaultTrackFallback(selectedTrack, fallbackTrack, fallbackIndex, selectedIndex) {
if (fallbackIndex < 0 || fallbackIndex === selectedIndex || !getUsableTrack(fallbackTrack)) return false;
if (!selectedTrack) return true;
var selectedLang = selectedTrack.languageCode || '';
var fallbackLang = fallbackTrack.languageCode || '';
if (!selectedLang || !fallbackLang) return true;
return String(selectedLang).split('-')[0] === String(fallbackLang).split('-')[0];
}
function getUsableTrack(track) {
return track && track.baseUrl ? track : null;
}
function getTrackName(track) {
if (!track) return 'track';
return track.name && track.name.simpleText ? track.name.simpleText : (track.vssId || 'track');
}
function formatTrackLabel(track, index) {
return '#' + index + ' ' + getTrackName(track) + ' (' + (track.languageCode || '') + ')';
}
function getPlayerResponse() {
var pageWindow = getPageWindow();
var player = getPlayer();
try {
if (player && typeof player.getPlayerResponse === 'function') {
var direct = player.getPlayerResponse();
if (direct) return direct;
}
} catch (err) {
logger.error('getPlayerResponse failed', err);
}
if (pageWindow.ytInitialPlayerResponse) return pageWindow.ytInitialPlayerResponse;
return extractPlayerResponseFromHtml(document.documentElement ? document.documentElement.innerHTML : '');
}
function getCaptionRenderer(playerResponse) {
if (!playerResponse || !playerResponse.captions) return null;
return playerResponse.captions.playerCaptionsTracklistRenderer || null;
}
function getCaptionTracks(playerResponse) {
var renderer = getCaptionRenderer(playerResponse);
return renderer && renderer.captionTracks ? renderer.captionTracks : [];
}
function getTranslationLanguages(playerResponse, tracks, sourceTrack) {
var renderer = getCaptionRenderer(playerResponse);
var raw = [];
var i;
if (renderer && renderer.translationLanguages) raw = raw.concat(renderer.translationLanguages);
if (sourceTrack && sourceTrack.translationLanguages) raw = raw.concat(sourceTrack.translationLanguages);
for (i = 0; i < (tracks || []).length; i++) {
if (tracks[i] && tracks[i].translationLanguages) raw = raw.concat(tracks[i].translationLanguages);
}
return normalizeTranslationLanguages(raw, sourceTrack);
}
function normalizeTranslationLanguages(raw, sourceTrack) {
var seen = {};
var result = [];
var sourceLang = sourceTrack && sourceTrack.languageCode ? String(sourceTrack.languageCode) : '';
var i;
for (i = 0; i < (raw || []).length; i++) {
var language = raw[i] || {};
var code = String(language.languageCode || language.lang || language.value || '').trim();
if (!code || seen[code]) continue;
if (sourceLang && code === sourceLang) continue;
seen[code] = true;
result.push({
languageCode: code,
name: readTranslationLanguageName(language) || code
});
}
return result;
}
function readTranslationLanguageName(language) {
if (!language) return '';
return readText(language.languageName) ||
readText(language.name) ||
readText(language.label) ||
String(language.displayName || language.title || '').trim();
}
function readText(value) {
if (!value) return '';
if (typeof value === 'string') return value.trim();
if (value.simpleText) return String(value.simpleText).trim();
if (value.runs && value.runs.length) {
return value.runs.map(function (run) {
return run && run.text ? run.text : '';
}).join('').trim();
}
return '';
}
function getDefaultCaptionTrackIndex(playerResponse) {
var renderer = getCaptionRenderer(playerResponse);
var audioTracks = renderer && renderer.audioTracks ? renderer.audioTracks : [];
if (!audioTracks.length) return -1;
var index = audioTracks[0] && typeof audioTracks[0].defaultCaptionTrackIndex === 'number' ? audioTracks[0].defaultCaptionTrackIndex : -1;
return index;
}
function getBestCaptionData(videoId) {
var playerResponse = getPlayerResponse();
var tracks = getCaptionTracks(playerResponse);
if (tracks.length) {
return Promise.resolve({
defaultTrackIndex: getDefaultCaptionTrackIndex(playerResponse),
playerResponse: playerResponse,
source: 'page',
tracks: tracks
});
}
return fetchPlayerResponseFromYoutubei(videoId).then(function (remoteResponse) {
return {
defaultTrackIndex: getDefaultCaptionTrackIndex(remoteResponse),
playerResponse: remoteResponse,
source: 'youtubei',
tracks: getCaptionTracks(remoteResponse)
};
}).catch(function (err) {
logger.error('youtubei player fallback failed', err);
return {
defaultTrackIndex: getDefaultCaptionTrackIndex(playerResponse),
playerResponse: playerResponse,
source: 'page-fallback',
tracks: tracks
};
});
}
function getInnertubeClientVersion() {
try {
var pageWindow = getPageWindow();
if (pageWindow.ytcfg && typeof pageWindow.ytcfg.get === 'function') {
return pageWindow.ytcfg.get('INNERTUBE_CLIENT_VERSION') || pageWindow.ytcfg.get('INNERTUBE_CONTEXT_CLIENT_VERSION') || '2.20250312.04.00';
}
} catch (err) {
logger.error('getInnertubeClientVersion failed', err);
}
return '2.20250312.04.00';
}
function getInnertubeVisitorData() {
try {
var pageWindow = getPageWindow();
if (pageWindow.ytcfg && typeof pageWindow.ytcfg.get === 'function') {
return pageWindow.ytcfg.get('VISITOR_DATA') || '';
}
} catch (err) {
logger.error('getInnertubeVisitorData failed', err);
}
return '';
}
function fetchPlayerResponseFromYoutubei(videoId) {
return postJson('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', {
context: buildInnertubeContext(),
videoId: videoId
}, buildInnertubeHeaders());
}
function postJson(url, body, headers) {
var payload = JSON.stringify(body);
var pageFetch = getPageFetch();
if (!pageFetch) {
return postJsonWithGM(url, payload, headers);
}
return pageFetch(url, {
method: 'POST',
body: payload,
headers: headers,
credentials: 'include',
cache: 'no-store'
}).then(function (res) {
if (res.status === 429) throw rateLimitError(url, 'fetch');
if (!res.ok) throw makeHttpError(res.status, url, 'fetch');
return res.json();
}).catch(function (err) {
if (err && err.status === 429) throw err;
return postJsonWithGM(url, payload, headers);
});
}
function postJsonWithGM(url, payload, headers) {
return new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'POST',
url: url,
data: payload,
headers: mergeHeaders(headers, {
'Referer': 'https://www.youtube.com/',
'User-Agent': getBrowserLikeUserAgent()
}),
onload: function (res) {
if (res.status === 429) {
reject(rateLimitError(url, 'gm'));
return;
}
if (res.status < 200 || res.status >= 300) {
reject(makeHttpError(res.status, url, 'gm'));
return;
}
try {
resolve(JSON.parse(res.responseText));
} catch (parseErr) {
reject(parseErr);
}
},
onerror: function () {
reject(makeHttpError('ERR', url, 'gm'));
}
});
});
}
function extractPlayerResponseFromHtml(html) {
var markers = ['ytInitialPlayerResponse = ', 'var ytInitialPlayerResponse = '];
var i;
for (i = 0; i < markers.length; i++) {
var marker = markers[i];
var markerIndex = html.indexOf(marker);
if (markerIndex === -1) continue;
var start = html.indexOf('{', markerIndex + marker.length);
if (start === -1) continue;
var depth = 0;
var inString = false;
var escaped = false;
var quote = '';
var j;
for (j = start; j < html.length; j++) {
var ch = html.charAt(j);
if (inString) {
if (escaped) {
escaped = false;
} else if (ch === '\\') {
escaped = true;
} else if (ch === quote) {
inString = false;
}
continue;
}
if (ch === '"' || ch === '\'') {
inString = true;
quote = ch;
continue;
}
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
try {
return JSON.parse(html.slice(start, j + 1));
} catch (err) {
logger.error('extractPlayerResponseFromHtml failed', err);
return null;
}
}
}
}
}
return null;
}
function makeHttpError(status, url, via) {
var err = new Error('HTTP ' + status);
err.status = status;
err.url = url;
err.via = via;
return err;
}
function rateLimitError(url, via) {
var err = new Error('HTTP 429');
err.status = 429;
err.url = url;
err.via = via;
err.rateLimited = true;
return err;
}
function mergeHeaders(baseHeaders, extraHeaders) {
var merged = {};
var key;
baseHeaders = baseHeaders || {};
extraHeaders = extraHeaders || {};
for (key in baseHeaders) merged[key] = baseHeaders[key];
for (key in extraHeaders) merged[key] = extraHeaders[key];
return merged;
}
function httpGet(url) {
return new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Referer': 'https://www.youtube.com/',
'User-Agent': getBrowserLikeUserAgent()
},
onload: function (res) {
if (res.status === 429) {
reject(rateLimitError(url, 'gm'));
return;
}
if (res.status < 200 || res.status >= 300) {
reject(makeHttpError(res.status, url, 'gm'));
return;
}
resolve(res.responseText);
},
onerror: function () {
reject(makeHttpError('ERR', url, 'gm'));
}
});
});
}
function fetchText(url) {
var pageFetch = getPageFetch();
if (!pageFetch) {
return httpGet(url);
}
return pageFetch(url, {
credentials: 'include',
cache: 'no-store'
}).then(function (res) {
if (res.status === 429) throw rateLimitError(url, 'fetch');
if (!res.ok) throw makeHttpError(res.status, url, 'fetch');
return res.text();
}).catch(function (err) {
if (err && err.status === 429) throw err;
return httpGet(url);
});
}
function fetchBestPair(sourceTrack, targetTrack, targetLang) {
clearFetchDiagnostics();
return fetchTrackCues(sourceTrack, null, 'source').then(function (sourceCues) {
if (sourceCues.length) {
return fetchTargetPair(sourceCues, targetTrack, sourceTrack, targetLang, '');
}
appendFetchDiagnostic('target', 'skip-source-empty');
return fetchTranscriptFallbackPair(sourceTrack, targetTrack, targetLang).then(function (fallbackResult) {
if (!fallbackResult.cuesA.length && !fallbackResult.cuesB.length) {
return {
cuesA: [],
cuesB: [],
fallback: fallbackResult.fallback || ''
};
}
if (!fallbackResult.cuesA.length || fallbackResult.cuesB.length) return fallbackResult;
return fetchTargetPair(fallbackResult.cuesA, targetTrack, sourceTrack, targetLang, fallbackResult.fallback || '');
});
});
}
function fetchTargetPair(sourceCues, targetTrack, sourceTrack, targetLang, fallback) {
var track = targetTrack || sourceTrack;
var language = targetTrack ? null : targetLang;
if (!track || !track.baseUrl) {
appendFetchDiagnostic('target', 'skip-no-target-track');
return Promise.resolve({
cuesA: sourceCues || [],
cuesB: [],
fallback: fallback || ''
});
}
return fetchTrackCues(track, language, 'target').then(function (targetCues) {
return {
cuesA: sourceCues || [],
cuesB: targetCues || [],
fallback: fallback || ''
};
}).catch(function (err) {
appendFetchDiagnostic('target', 'target-error-kept-source(' + formatError(err) + ')');
return {
cuesA: sourceCues || [],
cuesB: [],
fallback: fallback || ''
};
});
}
function fetchTrackCues(track, targetLang, label) {
var attempts = [];
function summarizeUrl(candidate) {
try {
var parsed = new URL(candidate.url, location.href);
var fmt = parsed.searchParams.get('fmt') || 'raw';
var lang = parsed.searchParams.get('lang') || '';
var tlang = parsed.searchParams.get('tlang') || '';
return fmt + ':' + lang + (tlang ? '->' + tlang : '') + (candidate.native ? ':native' : '');
} catch (err) {
return 'unknown';
}
}
function summarizeText(text) {
var compact = String(text || '').replace(/\s+/g, ' ').trim();
if (!compact) return 'empty';
return compact.slice(0, 48);
}
function tryCandidateSet(candidates, index) {
if (index >= candidates.length) {
setFetchDiagnostic(label, attempts.join(' | ') || 'no-attempt');
return Promise.resolve([]);
}
return fetchText(candidates[index].url).then(function (text) {
var cues = parseCaptionPayload(text);
attempts.push(summarizeUrl(candidates[index]) + ':ok(' + cues.length + ',' + summarizeText(text) + ')');
if (cues.length) {
setFetchDiagnostic(label, attempts.join(' | '));
return cues;
}
return tryCandidateSet(candidates, index + 1);
}).catch(function (err) {
attempts.push(summarizeUrl(candidates[index]) + ':err(' + formatError(err) + ')');
if (err && err.status === 429) {
setFetchDiagnostic(label, attempts.join(' | '));
throw err;
}
return tryCandidateSet(candidates, index + 1);
});
}
function tryNativeFallback(cause) {
return waitForNativeTimedTextHint(track).then(function (hasHint) {
var nativeCandidates = hasHint ? buildTimedTextCandidates(track, targetLang, true) : [];
if (!nativeCandidates.length) {
if (cause) throw cause;
return [];
}
return tryCandidateSet(nativeCandidates, 0).then(function (cues) {
if (cues.length || !cause) return cues;
throw cause;
});
});
}
var candidates = buildTimedTextCandidates(track, targetLang, false);
var hasNativeCandidates = candidates.some(function (candidate) {
return candidate.native;
});
return tryCandidateSet(candidates, 0).then(function (cues) {
if (cues.length || hasNativeCandidates) return cues;
return tryNativeFallback(null);
}).catch(function (err) {
if (err && err.status === 429 && !hasNativeCandidates) {
return tryNativeFallback(err);
}
throw err;
});
}
function buildTimedTextCandidates(track, targetLang, nativeOnly) {
var variants = [
{ fmt: 'json3' },
{ fmt: 'srv1' },
{ fmt: 'srv3' },
{ fmt: 'ttml' },
{ fmt: 'vtt' },
{}
];
var candidates = [];
var seen = {};
var hint = getNativeTimedTextHint(track);
var i;
if (hint && hint.params) {
for (i = 0; i < variants.length; i++) {
addCandidate(mergeObjects(hint.params, mergeTimedTextParams(targetLang, variants[i])), true);
}
}
if (!nativeOnly) {
for (i = 0; i < variants.length; i++) {
addCandidate(mergeTimedTextParams(targetLang, variants[i]), false);
}
}
return candidates;
function addCandidate(params, native) {
var url = buildTimedTextUrl(track.baseUrl, params);
if (seen[url]) return;
seen[url] = true;
candidates.push({
native: native,
url: url
});
}
}
function mergeTimedTextParams(targetLang, extraParams) {
var params = {};
var key;
if (targetLang) params.tlang = targetLang;
for (key in extraParams) params[key] = extraParams[key];
return params;
}
function mergeObjects(base, extra) {
var merged = {};
var key;
base = base || {};
extra = extra || {};
for (key in base) merged[key] = base[key];
for (key in extra) merged[key] = extra[key];
return merged;
}
function fetchTranscriptFallbackPair(sourceTrack, targetTrack, targetLang) {
return fetchTranscriptCues(getVideoId()).then(function (fallbackCues) {
if (fallbackCues.length) {
appendFetchDiagnostic('source', 'transcript-api:ok(' + fallbackCues.length + ')');
return {
cuesA: fallbackCues,
cuesB: [],
fallback: 'transcript-api'
};
}
appendFetchDiagnostic('source', 'transcript-api:empty');
return fetchTranscriptUiPair(sourceTrack, targetTrack, targetLang);
}).catch(function (apiErr) {
appendFetchDiagnostic('source', 'transcript-api:err(' + formatError(apiErr) + ')');
return fetchTranscriptUiPair(sourceTrack, targetTrack, targetLang);
}).catch(function (uiErr) {
appendFetchDiagnostic('source', 'transcript-ui:err(' + formatError(uiErr) + ')');
return {
cuesA: [],
cuesB: [],
fallback: ''
};
});
}
function fetchTranscriptUiPair(sourceTrack, targetTrack, targetLang) {
return withTranscriptPanel(function (panel) {
var originalTitle = getSelectedTranscriptLanguageTitle(panel) || '';
return loadTranscriptUiPair(panel, sourceTrack, targetTrack, targetLang).then(function (result) {
return restoreTranscriptLanguage(panel, originalTitle).then(function () {
return result;
}, function () {
return result;
});
}, function (err) {
return restoreTranscriptLanguage(panel, originalTitle).then(function () {
throw err;
}, function () {
throw err;
});
});
});
}
function withTranscriptPanel(task) {
var panelInfo = null;
return openTranscriptPanel().then(function (info) {
panelInfo = info;
return task(info.panel, info);
}).finally(function () {
if (!panelInfo) return;
if (panelInfo.openedByScript) {
closeTranscriptPanel(panelInfo.panel);
} else {
removeHiddenTranscriptPanelStyle();
}
});
}
function loadTranscriptUiPair(panel, sourceTrack, targetTrack, targetLang) {
var sourceInfo = null;
var targetInfo = null;
return readTranscriptUiCues(panel, sourceTrack, sourceTrack ? sourceTrack.languageCode : '', true).then(function (value) {
sourceInfo = value;
appendFetchDiagnostic('source', 'transcript-ui:ok(' + value.cues.length + ',' + (value.title || 'current') + ')');
return readTranscriptUiCues(panel, targetTrack, targetTrack ? targetTrack.languageCode : targetLang, false).then(function (targetValue) {
targetInfo = targetValue;
if (targetValue.cues.length) {
appendFetchDiagnostic('target', 'transcript-ui:ok(' + targetValue.cues.length + ',' + (targetValue.title || targetLang || 'target') + ')');
} else if (targetValue.title) {
appendFetchDiagnostic('target', 'transcript-ui:empty(' + targetValue.title + ')');
} else if (targetLang) {
appendFetchDiagnostic('target', 'transcript-ui:skip(' + targetLang + ')');
}
return {
cuesA: sourceInfo.cues,
cuesB: targetInfo.cues,
fallback: 'transcript-ui'
};
});
}).catch(function (err) {
throw err;
});
}
function readTranscriptUiCues(panel, track, languageCode, required) {
var options = getTranscriptLanguageOptions(panel);
var desiredTitle = resolveTranscriptLanguageTitle(options, track, languageCode);
var currentTitle = getSelectedTranscriptLanguageTitle(panel);
var hasLanguageOptions = !!options.length;
if (!desiredTitle && !hasLanguageOptions) {
if (required) {
return waitFor(function () {
var currentCues = extractTranscriptPanelCues(panel);
return currentCues.length ? currentCues : null;
}, 3000, 120).then(function (cues) {
if (!cues || !cues.length) throw new Error('Transcript UI empty: current');
return {
cues: cues,
title: currentTitle || 'current'
};
});
}
return Promise.resolve({
cues: [],
title: ''
});
}
if (!desiredTitle) {
if (required) {
if (!track && currentTitle) {
desiredTitle = currentTitle;
} else if (currentTitle && transcriptTitleMatches(currentTitle, track, languageCode)) {
desiredTitle = currentTitle;
} else {
throw new Error('Transcript language not found: ' + (track ? getTrackName(track) : (languageCode || 'source')));
}
} else {
return Promise.resolve({
cues: [],
title: ''
});
}
}
return ensureTranscriptLanguage(panel, desiredTitle).then(function () {
return waitFor(function () {
var cues = extractTranscriptPanelCues(panel);
return cues.length ? cues : null;
}, 3000, 120);
}).then(function (cues) {
if (!cues || !cues.length) {
if (required) throw new Error('Transcript UI empty: ' + desiredTitle);
return {
cues: [],
title: desiredTitle
};
}
return {
cues: cues,
title: desiredTitle
};
});
}
function openTranscriptPanel() {
var ready = getReadyTranscriptPanel();
if (ready) {
return Promise.resolve({
panel: ready,
openedByScript: false
});
}
applyHiddenTranscriptPanelStyle();
return tryOpenTranscriptPanelLoop(Date.now() + 12000).then(function (panel) {
if (panel) {
return {
panel: panel,
openedByScript: true
};
}
removeHiddenTranscriptPanelStyle();
throw new Error('Transcript button not found');
});
}
function tryOpenTranscriptPanelLoop(deadline) {
return tryOpenTranscriptPanelOnce().then(function (panel) {
if (panel) return panel;
if (Date.now() >= deadline) return null;
return wait(250).then(function () {
return tryOpenTranscriptPanelLoop(deadline);
});
});
}
function tryOpenTranscriptPanelOnce() {
var panel = getReadyTranscriptPanel();
if (panel) return Promise.resolve(panel);
return tryClickTranscriptAndWait(findTranscriptTrigger(), 3000, 120).then(function (directPanel) {
if (directPanel) return directPanel;
var menuButton = document.querySelector(CONFIG.selectors.transcriptMenuButton);
if (!menuButton) return null;
menuButton.click();
return waitFor(function () {
return findTranscriptMenuItem();
}, 2000, 100).then(function (menuItem) {
return tryClickTranscriptAndWait(menuItem, 3000, 120);
});
}).then(function (menuPanel) {
if (menuPanel) return menuPanel;
return tryClickTranscriptAndWait(document.querySelector(CONFIG.selectors.transcriptDescriptionButton), 3000, 120);
});
}
function tryClickTranscriptAndWait(element, timeoutMs, intervalMs) {
if (!element) return Promise.resolve(null);
element.click();
return waitFor(function () {
return getReadyTranscriptPanel();
}, timeoutMs || 3000, intervalMs || 120);
}
function getTranscriptPanel() {
return document.querySelector(CONFIG.selectors.transcriptPanel);
}
function getReadyTranscriptPanel() {
var panels = document.querySelectorAll(CONFIG.selectors.transcriptPanel);
var i;
for (i = 0; i < panels.length; i++) {
if (hasTranscriptContent(panels[i])) return panels[i];
}
return null;
}
function closeTranscriptPanel(panel) {
var root = panel || getTranscriptPanel();
var selector = '#visibility-button ytd-button-renderer button, #visibility-button yt-button-shape button, #dismiss-button button, ytd-engagement-panel-title-header-renderer #dismiss-button button, ytd-engagement-panel-title-header-renderer #dismiss-button, yt-icon-button#dismiss-button button, yt-icon-button#dismiss-button';
var header = root ? root.querySelector('ytd-engagement-panel-title-header-renderer, #header') : null;
var button = header ? header.querySelector(selector) : null;
if (!button && root) button = root.querySelector(selector);
if (!button) button = document.querySelector(selector);
if (button) button.click();
deferHiddenTranscriptStyleRemoval(root);
}
function deferHiddenTranscriptStyleRemoval(panel) {
var started = Date.now();
function tick() {
if (isTranscriptPanelClosed(panel) || Date.now() - started >= 1800) {
removeHiddenTranscriptPanelStyle();
return;
}
setTimeout(tick, 120);
}
tick();
}
function isTranscriptPanelClosed(panel) {
var currentPanel = panel || getTranscriptPanel();
if (!currentPanel || !document.contains(currentPanel) || currentPanel.hidden || currentPanel.getAttribute('aria-hidden') === 'true') return true;
var style = window.getComputedStyle(currentPanel);
return style.display === 'none' || style.visibility === 'hidden';
}
function applyHiddenTranscriptPanelStyle() {
if (document.getElementById(CONFIG.ids.hiddenTranscriptStyle)) return;
var style = document.createElement('style');
style.id = CONFIG.ids.hiddenTranscriptStyle;
style.textContent = '#panels ytd-engagement-panel-section-list-renderer[visibility=\"ENGAGEMENT_PANEL_VISIBILITY_EXPANDED\"]{position:fixed!important;opacity:0!important;pointer-events:none!important}';
(document.head || document.documentElement || document.body).appendChild(style);
}
function removeHiddenTranscriptPanelStyle() {
var node = document.getElementById(CONFIG.ids.hiddenTranscriptStyle);
if (node) node.remove();
}
function hasTranscriptContent(panel) {
if (!panel || panel.hidden || panel.getAttribute('aria-hidden') === 'true') return false;
return !!panel.querySelector(CONFIG.selectors.transcriptRenderer + ', ' + CONFIG.selectors.transcriptSegment);
}
function findTranscriptTrigger() {
var chipTrigger = findTranscriptChipButton();
if (chipTrigger) return chipTrigger;
return findTranscriptMenuItem();
}
function findTranscriptChipButton() {
var items = document.querySelectorAll(CONFIG.selectors.transcriptChipButton + ', [aria-label], [title]');
var i;
for (i = 0; i < items.length; i++) {
if (matchTranscriptLabel(items[i])) return items[i];
}
return null;
}
function findTranscriptMenuItem() {
var items = document.querySelectorAll(CONFIG.selectors.transcriptMenuItems);
var i;
for (i = 0; i < items.length; i++) {
if (matchTranscriptLabel(items[i])) return items[i];
}
return null;
}
function matchTranscriptLabel(node) {
if (!node) return false;
var text = [node.getAttribute && node.getAttribute('aria-label'), node.getAttribute && node.getAttribute('title'), node.textContent].join(' ');
return TRANSCRIPT_LABEL_PATTERN.test(String(text || '').trim());
}
function describeTranscriptTrigger() {
var trigger = findTranscriptTrigger();
if (!trigger) return 'none';
var parts = [];
var tagName = trigger.tagName ? trigger.tagName.toLowerCase() : 'node';
parts.push(tagName);
var ariaLabel = trigger.getAttribute ? trigger.getAttribute('aria-label') : '';
var title = trigger.getAttribute ? trigger.getAttribute('title') : '';
var text = String(trigger.textContent || '').replace(/\s+/g, ' ').trim();
var label = ariaLabel || title || text || '';
if (label) parts.push(label.slice(0, 48));
return parts.join(':');
}
function getTranscriptRendererData(panel) {
if (!panel) return null;
var transcriptRenderer = panel.querySelector(CONFIG.selectors.transcriptRenderer);
if (!transcriptRenderer) return null;
if (transcriptRenderer.__data && transcriptRenderer.__data.data) return transcriptRenderer.__data.data;
if (transcriptRenderer.data) return transcriptRenderer.data;
if (transcriptRenderer.__dataHost && transcriptRenderer.__dataHost.__data) return transcriptRenderer.__dataHost.__data;
return null;
}
function extractTranscriptPanelCues(panel) {
var cues = extractTranscriptPanelCuesFromData(panel);
if (cues.length) return cues;
return extractTranscriptPanelCuesFromDom(panel);
}
function extractTranscriptPanelCuesFromData(panel) {
var transcriptData = getTranscriptRendererData(panel);
var segments = transcriptData && transcriptData.content && transcriptData.content.transcriptSearchPanelRenderer && transcriptData.content.transcriptSearchPanelRenderer.body && transcriptData.content.transcriptSearchPanelRenderer.body.transcriptSegmentListRenderer ? transcriptData.content.transcriptSearchPanelRenderer.body.transcriptSegmentListRenderer.initialSegments : null;
var cues = [];
var i;
if (!segments || !segments.length) return cues;
for (i = 0; i < segments.length; i++) {
var item = segments[i] && segments[i].transcriptSegmentRenderer;
if (!item) continue;
var startMs = parseInt(item.startMs || '0', 10);
var endMs = parseInt(item.endMs || '0', 10);
var text = readRunsText(item.snippet && item.snippet.runs);
if (!text) continue;
cues.push({
start: startMs / 1000,
end: (endMs || startMs + 5000) / 1000,
text: text
});
}
normalizeCueEnds(cues);
return cues;
}
function extractTranscriptPanelCuesFromDom(panel) {
var renderers = panel.querySelectorAll(CONFIG.selectors.transcriptSegment);
var cues = [];
var i;
for (i = 0; i < renderers.length; i++) {
var renderer = renderers[i];
var textNode = renderer.querySelector(CONFIG.selectors.transcriptText);
var text = textNode ? String(textNode.textContent || '').trim() : '';
var targetId = renderer.getAttribute('target-id');
var startMs = 0;
var endMs = 0;
var parts;
if (!text) continue;
if (!targetId && renderer.data && renderer.data.targetId) targetId = renderer.data.targetId;
if (!targetId && renderer.__data && renderer.__data.data && renderer.__data.data.targetId) targetId = renderer.__data.data.targetId;
if (renderer.tagName && renderer.tagName.toLowerCase() === 'transcript-segment-view-model') {
startMs = parseTranscriptTimeToMs(renderer.querySelector(CONFIG.selectors.transcriptTime));
} else if (targetId) {
parts = targetId.split('.');
startMs = parseInt(parts[parts.length - 2] || '0', 10);
endMs = parseInt(parts[parts.length - 1] || '0', 10);
} else {
startMs = parseTranscriptTimeToMs(renderer.querySelector(CONFIG.selectors.transcriptTime));
}
cues.push({
start: startMs / 1000,
end: (endMs || startMs + 5000) / 1000,
text: text.replace(/\s+/g, ' ').trim()
});
}
normalizeCueEnds(cues);
return cues;
}
function normalizeCueEnds(cues) {
var i;
for (i = 0; i < cues.length; i++) {
if (cues[i].end > cues[i].start) continue;
cues[i].end = i + 1 < cues.length ? cues[i + 1].start : cues[i].start + 5;
}
}
function parseTranscriptTimeToMs(node) {
var text = node ? String(node.textContent || '').trim() : '';
if (!text) return 0;
var parts = text.split(':');
var nums = [];
var i;
for (i = 0; i < parts.length; i++) nums.push(parseInt(parts[i] || '0', 10) || 0);
if (nums.length === 3) return ((nums[0] * 3600) + (nums[1] * 60) + nums[2]) * 1000;
if (nums.length === 2) return ((nums[0] * 60) + nums[1]) * 1000;
return (nums[0] || 0) * 1000;
}
function getTranscriptLanguageOptions(panel) {
var transcriptData = getTranscriptRendererData(panel);
var subMenuItems = transcriptData && transcriptData.content && transcriptData.content.transcriptSearchPanelRenderer && transcriptData.content.transcriptSearchPanelRenderer.footer && transcriptData.content.transcriptSearchPanelRenderer.footer.transcriptFooterRenderer && transcriptData.content.transcriptSearchPanelRenderer.footer.transcriptFooterRenderer.languageMenu && transcriptData.content.transcriptSearchPanelRenderer.footer.transcriptFooterRenderer.languageMenu.sortFilterSubMenuRenderer ? transcriptData.content.transcriptSearchPanelRenderer.footer.transcriptFooterRenderer.languageMenu.sortFilterSubMenuRenderer.subMenuItems : null;
var options = [];
var i;
if (!subMenuItems || !subMenuItems.length) return options;
for (i = 0; i < subMenuItems.length; i++) {
options.push({
title: subMenuItems[i].title || '',
selected: !!subMenuItems[i].selected
});
}
return options;
}
function getSelectedTranscriptLanguageTitle(panel) {
var options = getTranscriptLanguageOptions(panel);
var i;
for (i = 0; i < options.length; i++) {
if (options[i].selected && options[i].title) return options[i].title;
}
var selectors = ['#label-text.yt-dropdown-menu', '[aria-selected=\"true\"]', '.iron-selected'];
for (i = 0; i < selectors.length; i++) {
var node = panel.querySelector(selectors[i]);
if (node && String(node.textContent || '').trim()) return String(node.textContent || '').trim();
}
return '';
}
function ensureTranscriptLanguage(panel, title) {
var current = getSelectedTranscriptLanguageTitle(panel);
if (!title || normalizeLangLabel(current) === normalizeLangLabel(title)) {
return Promise.resolve(false);
}
var dropdownButton = panel.querySelector(CONFIG.selectors.transcriptLanguageDropdown);
if (!dropdownButton) {
return waitFor(function () {
return panel.querySelector(CONFIG.selectors.transcriptLanguageDropdown);
}, 2000, 120).then(function (button) {
if (!button) throw new Error('Transcript language selector not found');
return switchTranscriptLanguageWithButton(panel, button, title);
});
}
return switchTranscriptLanguageWithButton(panel, dropdownButton, title);
}
function switchTranscriptLanguageWithButton(panel, button, title) {
button.click();
return wait(300).then(function () {
var listboxes = document.querySelectorAll(CONFIG.selectors.transcriptVisibleListboxes);
var i;
var j;
for (i = 0; i < listboxes.length; i++) {
var items = listboxes[i].querySelectorAll('tp-yt-paper-item, yt-formatted-string');
for (j = 0; j < items.length; j++) {
if (normalizeLangLabel(items[j].textContent) === normalizeLangLabel(title)) {
var target = items[j].closest ? items[j].closest('tp-yt-paper-item') : null;
(target || items[j]).click();
return wait(900).then(function () {
return waitFor(function () {
return normalizeLangLabel(getSelectedTranscriptLanguageTitle(panel)) === normalizeLangLabel(title) ? true : null;
}, 2500, 120).then(function (matched) {
if (!matched) throw new Error('Transcript language switch timed out: ' + title);
return true;
});
});
}
}
}
document.body.click();
throw new Error('Transcript language option not found: ' + title);
});
}
function restoreTranscriptLanguage(panel, title) {
if (!panel || !title) return Promise.resolve();
var current = getSelectedTranscriptLanguageTitle(panel);
if (!current || normalizeLangLabel(current) === normalizeLangLabel(title)) return Promise.resolve();
return ensureTranscriptLanguage(panel, title).catch(function () {});
}
function resolveTranscriptLanguageTitle(options, track, languageCode) {
var aliases = getLanguageAliases(languageCode, track ? getTrackName(track) : '');
var i;
var j;
if (track) {
var exactName = normalizeLangLabel(getTrackName(track));
for (i = 0; i < options.length; i++) {
if (normalizeLangLabel(options[i].title) === exactName) return options[i].title;
}
}
for (i = 0; i < aliases.length; i++) {
var alias = normalizeLangLabel(aliases[i]);
if (!alias) continue;
for (j = 0; j < options.length; j++) {
var optionTitle = normalizeLangLabel(options[j].title);
if (optionTitle === alias || optionTitle.indexOf(alias) !== -1 || alias.indexOf(optionTitle) !== -1) return options[j].title;
}
}
return '';
}
function transcriptTitleMatches(title, track, languageCode) {
return !!resolveTranscriptLanguageTitle([{ title: title, selected: true }], track, languageCode);
}
function getLanguageAliases(languageCode, displayName) {
var aliases = [];
var normalized = String(languageCode || '').toLowerCase();
var prefix = normalized.split('-')[0];
if (displayName) aliases.push(displayName);
if (languageCode) aliases.push(languageCode);
if (prefix && prefix !== normalized) aliases.push(prefix);
if (normalized === 'zh-hant') {
aliases.push('中文(繁體字)', '繁體中文', '繁体中文', '繁體字', '繁体', 'traditional chinese', 'traditional');
} else if (normalized === 'zh-hans') {
aliases.push('中文(简体)', '中文(簡體)', '简体中文', '簡體中文', '简体', '簡體', 'simplified chinese', 'simplified');
} else if (prefix === 'zh') {
aliases.push('中文', 'chinese');
} else if (prefix === 'en') {
aliases.push('English', '英文', '英语', '英語');
} else if (prefix === 'ja') {
aliases.push('Japanese', '日文', '日语', '日語', '日本語');
} else if (prefix === 'ko') {
aliases.push('Korean', '韩文', '韓文', '韩语', '韓語', '한국어');
} else if (prefix === 'es') {
aliases.push('Spanish', 'Español', '西班牙语', '西班牙語');
}
return aliases;
}
function normalizeLangLabel(value) {
return String(value || '').toLowerCase().replace(/\s+/g, '').replace(/[()()._-]/g, '');
}
function wait(delayMs) {
return new Promise(function (resolve) {
setTimeout(resolve, delayMs);
});
}
function waitFor(getValue, timeoutMs, intervalMs) {
var started = Date.now();
return new Promise(function (resolve) {
function tick() {
var value = null;
try {
value = getValue();
} catch (err) {
value = null;
}
if (value) {
resolve(value);
return;
}
if (Date.now() - started >= timeoutMs) {
resolve(null);
return;
}
setTimeout(tick, intervalMs);
}
tick();
});
}
function fetchTranscriptCues(videoId) {
return fetchTranscriptResponse(videoId).then(function (response) {
return parseTranscriptResponse(response);
});
}
function fetchTranscriptResponse(videoId) {
return postJson('https://www.youtube.com/youtubei/v1/next?prettyPrint=false', {
context: buildInnertubeContext(),
videoId: videoId
}, buildInnertubeHeaders()).then(function (nextResponse) {
var endpoint = findNestedByKey(nextResponse, 'getTranscriptEndpoint');
if (!endpoint || !endpoint.params) {
throw new Error('Transcript endpoint not found');
}
return postJson('https://www.youtube.com/youtubei/v1/get_transcript?prettyPrint=false', {
context: buildInnertubeContext(),
params: endpoint.params
}, buildInnertubeHeaders());
});
}
function parseTranscriptResponse(response) {
var listRenderer = findNestedByKey(response, 'transcriptSegmentListRenderer');
var segments = listRenderer && listRenderer.initialSegments ? listRenderer.initialSegments : [];
var cues = [];
var i;
for (i = 0; i < segments.length; i++) {
var item = segments[i] && segments[i].transcriptSegmentRenderer;
if (!item) continue;
var startMs = parseInt(item.startMs || '0', 10);
var endMs = parseInt(item.endMs || '0', 10);
var text = readRunsText(item.snippet && item.snippet.runs);
if (!text) continue;
cues.push({
start: startMs / 1000,
end: (endMs || startMs) / 1000,
text: text
});
}
return cues;
}
function readRunsText(runs) {
if (!runs || !runs.length) return '';
var parts = [];
var i;
for (i = 0; i < runs.length; i++) {
if (runs[i] && runs[i].text) parts.push(runs[i].text);
}
return parts.join('').replace(/\s+/g, ' ').trim();
}
function findNestedByKey(value, key) {
if (!value || typeof value !== 'object') return null;
if (Object.prototype.hasOwnProperty.call(value, key)) return value[key];
var prop;
for (prop in value) {
if (!Object.prototype.hasOwnProperty.call(value, prop)) continue;
var nested = findNestedByKey(value[prop], key);
if (nested) return nested;
}
return null;
}
function parseCaptionPayload(text) {
var body = String(text || '').trim();
if (!body) return [];
if (body.charAt(0) === '{') return parseJson3(body);
if (body.charAt(0) === '<') return parseXml(body);
return parseVtt(body);
}
function parseVtt(text) {
var cues = [];
var lines = String(text || '').replace(/\r/g, '').split('\n');
var i = 0;
function toSec(value) {
var parts = value.split(':');
var nums = [];
var k;
for (k = 0; k < parts.length; k++) nums.push(parseFloat(parts[k]));
if (nums.length === 3) return nums[0] * 3600 + nums[1] * 60 + nums[2];
if (nums.length === 2) return nums[0] * 60 + nums[1];
return nums[0] || 0;
}
while (i < lines.length) {
var line = lines[i].trim();
if (!line) {
i += 1;
continue;
}
if (line.indexOf('WEBVTT') === 0) {
i += 1;
continue;
}
if (/^\d+$/.test(line)) {
i += 1;
line = (lines[i] || '').trim();
}
if (line.indexOf('-->') === -1) {
i += 1;
continue;
}
var parts = line.split('-->');
var start = toSec(parts[0].trim().split(' ')[0].replace(',', '.'));
var end = toSec(parts[1].trim().split(' ')[0].replace(',', '.'));
i += 1;
var textLines = [];
while (i < lines.length && lines[i].trim() !== '') {
textLines.push(lines[i].replace(/<[^>]+>/g, '').trim());
i += 1;
}
var cueText = textLines.join('\n').trim();
if (cueText) cues.push({ start: start, end: end, text: cueText });
}
return cues;
}
function parseJson3(text) {
var data = JSON.parse(text);
var events = data && data.events ? data.events : [];
var cues = [];
var i;
for (i = 0; i < events.length; i++) {
var event = events[i];
if (!event || !event.segs || !event.segs.length) continue;
var start = (event.tStartMs || 0) / 1000;
var end = ((event.tStartMs || 0) + (event.dDurationMs || 0)) / 1000;
var segs = [];
var j;
for (j = 0; j < event.segs.length; j++) segs.push(event.segs[j].utf8 || '');
var cueText = segs.join('').replace(/\n+/g, '\n').trim();
if (cueText) cues.push({ start: start, end: end, text: cueText });
}
return cues;
}
function parseXml(text) {
var xml = new DOMParser().parseFromString(text, 'text/xml');
if (xml.querySelector('parsererror')) throw new Error('XML parse error');
var nodes = xml.querySelectorAll('p, text');
var cues = [];
var i;
function readTime(node, nameA, nameB) {
var raw = node.getAttribute(nameA);
if (raw == null && nameB) raw = node.getAttribute(nameB);
return raw == null ? 0 : parseFloat(raw);
}
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
var start = readTime(node, 't', 'start');
var dur = readTime(node, 'd', 'dur');
if (node.tagName.toLowerCase() === 'p') {
start = start / 1000;
dur = dur / 1000;
}
var cueText = String(node.textContent || '').replace(/\s+/g, ' ').trim();
if (!cueText) continue;
cues.push({ start: start, end: start + dur, text: cueText });
}
return cues;
}
function formatError(err) {
if (!err) return 'unknown';
if (typeof err === 'string') return err;
var parts = [];
if (err.message) parts.push(err.message);
if (err.status != null) parts.push('status=' + err.status);
if (err.via) parts.push('via=' + err.via);
return parts.join(' | ') || String(err);
}
})();