Summarizes YouTube videos using Gemini API with a GUI for API key, model, and language selection (with RTL support).
当前为
// ==UserScript==
// @name YouTube Gemini Summarizer
// @namespace http://tampermonkey.net/
// @version 0.2.0
// @description Summarizes YouTube videos using Gemini API with a GUI for API key, model, and language selection (with RTL support).
// @author kobaltGIT
// @match https://www.youtube.com/watch*
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI0NCIgaGVpZ2h0PSI0NCIgcng9IjUiIHJ5PSI1IiBmaWxsPSIjNENBRjUwIi8+PGxpbmUgeDE9IjE4IiB5MT0iMjQiIHgyPSI0NiIgeTI9IjI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiLz48bGluZSB4MT0iMTgiIHkxPSIzMiIgeDI9IjQ2IiB5Mj0iMzIiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIvPjxsaW5lIHgxPSIxOCIgeTE9IjQwIiB4Mj0iMzQiIHkyPSI0MCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIzIi8+PHBvbHlnb24gcG9pbnRzPSIzOCwzNyA0Niw0MCAzOCw0MyIgZmlsbD0id2hpdGUiLz48L3N2Zz4=
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect generativelanguage.googleapis.com
// ==/UserScript==
(function() {
'use strict';
console.log('MY SCRIPT IS TRYING TO RUN (v0.2.0)');
const SCRIPT_PREFIX = 'yt_gemini_summarizer_';
const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
const HIDE_BUTTON_DELAY = 2000;
const HIDE_BUTTON_DELAY_FAST = 500;
const DIAGNOSTIC_INTERVAL_MS = 3000;
const SUPPORTED_LANGUAGES = [
{ value: 'Russian', display: 'Russian', isRTL: false },
{ value: 'English', display: 'English', isRTL: false },
{ value: 'German', display: 'German', isRTL: false },
{ value: 'French', display: 'French', isRTL: false },
{ value: 'Italian', display: 'Italian', isRTL: false },
{ value: 'Spanish', display: 'Spanish', isRTL: false },
{ value: 'Hebrew', display: 'Hebrew', isRTL: true },
{ value: 'Yiddish', display: 'Yiddish', isRTL: true },
{ value: 'Arabic', display: 'Arabic', isRTL: true },
{ value: 'Chinese', display: 'Chinese', isRTL: false },
{ value: 'Korean', display: 'Korean', isRTL: false },
{ value: 'Japanese', display: 'Japanese', isRTL: false },
{ value: 'Czech', display: 'Czech', isRTL: false },
{ value: 'Polish', display: 'Polish', isRTL: false }
];
const DEFAULT_LANGUAGE = 'Russian'; // This value must match one of the 'value' properties above
let apiKey = '';
let models = [];
let selectedModel = '';
let selectedLanguage = DEFAULT_LANGUAGE;
let panel, apiKeyInput, modelSelect, languageSelect, summarizeButton, summaryOutput, statusDiv, fetchModelsButton;
let togglePanelButton;
let isPanelVisible = false;
let playerContainer = null;
let hideButtonTimer = null;
let diagnosticIntervalId = null;
function loadSettings() {
apiKey = GM_getValue(SCRIPT_PREFIX + 'api_key', '');
selectedModel = GM_getValue(SCRIPT_PREFIX + 'selected_model', '');
selectedLanguage = GM_getValue(SCRIPT_PREFIX + 'selected_language', DEFAULT_LANGUAGE);
isPanelVisible = GM_getValue(SCRIPT_PREFIX + 'is_panel_visible', false);
if (apiKeyInput) apiKeyInput.value = apiKey;
if (languageSelect) {
languageSelect.value = selectedLanguage;
}
}
function saveSettings() {
GM_setValue(SCRIPT_PREFIX + 'api_key', apiKey);
GM_setValue(SCRIPT_PREFIX + 'selected_model', selectedModel);
GM_setValue(SCRIPT_PREFIX + 'selected_language', selectedLanguage);
GM_setValue(SCRIPT_PREFIX + 'is_panel_visible', isPanelVisible);
}
function createGUI() {
panel = document.createElement('div');
panel.id = SCRIPT_PREFIX + 'panel';
panel.style.display = isPanelVisible ? 'block' : 'none';
const title = document.createElement('h3');
title.textContent = 'Gemini Video Summarizer';
panel.appendChild(title);
const apiKeyDiv = document.createElement('div');
const apiKeyLabel = document.createElement('label');
apiKeyLabel.htmlFor = SCRIPT_PREFIX + 'api_key_input';
apiKeyLabel.textContent = 'Gemini API Key:';
apiKeyDiv.appendChild(apiKeyLabel);
apiKeyInput = document.createElement('input');
apiKeyInput.type = 'password';
apiKeyInput.id = SCRIPT_PREFIX + 'api_key_input';
apiKeyDiv.appendChild(apiKeyInput);
fetchModelsButton = document.createElement('button');
fetchModelsButton.id = SCRIPT_PREFIX + 'fetch_models_button';
fetchModelsButton.textContent = 'Save & Fetch Models';
apiKeyDiv.appendChild(fetchModelsButton);
panel.appendChild(apiKeyDiv);
const modelDiv = document.createElement('div');
const modelLabel = document.createElement('label');
modelLabel.htmlFor = SCRIPT_PREFIX + 'model_select';
modelLabel.textContent = 'Select Model:';
modelDiv.appendChild(modelLabel);
modelSelect = document.createElement('select');
modelSelect.id = SCRIPT_PREFIX + 'model_select';
modelDiv.appendChild(modelSelect);
panel.appendChild(modelDiv);
const languageDiv = document.createElement('div');
const languageLabel = document.createElement('label');
languageLabel.htmlFor = SCRIPT_PREFIX + 'language_select';
languageLabel.textContent = 'Select Summary Language:';
languageDiv.appendChild(languageLabel);
languageSelect = document.createElement('select');
languageSelect.id = SCRIPT_PREFIX + 'language_select';
SUPPORTED_LANGUAGES.forEach(lang => {
const option = document.createElement('option');
option.value = lang.value;
option.textContent = lang.display; // Now using English display name
languageSelect.appendChild(option);
});
languageSelect.value = selectedLanguage;
languageDiv.appendChild(languageSelect);
panel.appendChild(languageDiv);
const summarizeDiv = document.createElement('div');
summarizeButton = document.createElement('button');
summarizeButton.id = SCRIPT_PREFIX + 'summarize_button';
summarizeButton.textContent = 'Summarize Video';
summarizeButton.disabled = true;
summarizeDiv.appendChild(summarizeButton);
panel.appendChild(summarizeDiv);
statusDiv = document.createElement('div');
statusDiv.id = SCRIPT_PREFIX + 'status_div';
statusDiv.style.marginTop = '5px';
statusDiv.style.fontStyle = 'italic';
panel.appendChild(statusDiv);
const summaryTitle = document.createElement('h4');
summaryTitle.textContent = 'Summary:';
panel.appendChild(summaryTitle);
summaryOutput = document.createElement('textarea');
summaryOutput.id = SCRIPT_PREFIX + 'summary_output';
summaryOutput.rows = 10;
summaryOutput.readOnly = true;
summaryOutput.dir = 'ltr'; // Default LTR
summaryOutput.style.textAlign = 'left'; // Default left align
panel.appendChild(summaryOutput);
document.body.appendChild(panel);
fetchModelsButton.addEventListener('click', handleFetchModelsClick);
modelSelect.addEventListener('change', (e) => {
selectedModel = e.target.value;
saveSettings();
updateSummarizeButtonState();
});
languageSelect.addEventListener('change', (e) => {
selectedLanguage = e.target.value;
saveSettings();
// Update text direction if summary already exists (optional, or just on new summary)
const languageInfo = SUPPORTED_LANGUAGES.find(lang => lang.value === selectedLanguage);
if (summaryOutput.value.trim() !== "") { // Only if there's text
if (languageInfo && languageInfo.isRTL) {
summaryOutput.dir = 'rtl';
summaryOutput.style.textAlign = 'right';
} else {
summaryOutput.dir = 'ltr';
summaryOutput.style.textAlign = 'left';
}
}
});
summarizeButton.addEventListener('click', handleSummarizeClick);
}
function createToggleButton() {
togglePanelButton = document.createElement('button');
togglePanelButton.id = SCRIPT_PREFIX + 'toggle_button';
togglePanelButton.textContent = 'Summarize';
togglePanelButton.title = 'Toggle Summarizer Panel';
togglePanelButton.addEventListener('click', handleTogglePanelClick);
}
function handleTogglePanelClick() {
isPanelVisible = !isPanelVisible;
panel.style.display = isPanelVisible ? 'block' : 'none';
saveSettings();
console.log(SCRIPT_PREFIX + "Panel visibility toggled. Is visible:", isPanelVisible);
if (isPanelVisible && panel.matches(':hover')) {
showToggleButton();
} else if (!isPanelVisible && playerContainer && !playerContainer.matches(':hover')) {
startHideButtonTimer(0);
}
}
function showToggleButton() {
if (!togglePanelButton) {
console.warn(SCRIPT_PREFIX + "showToggleButton called but togglePanelButton is null!");
return;
}
clearTimeout(hideButtonTimer);
hideButtonTimer = null;
if (togglePanelButton.style.opacity !== '1') {
togglePanelButton.style.opacity = '1';
togglePanelButton.style.pointerEvents = 'auto';
// console.log(SCRIPT_PREFIX + "Toggle button SHOWN (opacity 1, pointerEvents auto).");
}
}
function hideToggleButton() {
if (!togglePanelButton) {
console.warn(SCRIPT_PREFIX + "hideToggleButton called but togglePanelButton is null!");
return;
}
if (togglePanelButton.style.opacity !== '0') {
togglePanelButton.style.opacity = '0';
togglePanelButton.style.pointerEvents = 'none';
// console.log(SCRIPT_PREFIX + "Toggle button HIDDEN (opacity 0, pointerEvents none).");
}
}
function startHideButtonTimer(delay = HIDE_BUTTON_DELAY) {
clearTimeout(hideButtonTimer);
hideButtonTimer = setTimeout(() => {
if (playerContainer && playerContainer.matches(':hover')) return;
if (panel && panel.matches(':hover') && isPanelVisible) return;
if (togglePanelButton && togglePanelButton.matches(':hover')) return;
hideToggleButton();
}, delay);
}
function addStyles() {
GM_addStyle(`
#${SCRIPT_PREFIX}panel {
position: fixed; top: 60px; right: 10px; width: 350px;
background-color: #f9f9f9; border: 1px solid #ccc; padding: 15px;
z-index: 9999; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
font-family: Arial, sans-serif; font-size: 13px;
}
#${SCRIPT_PREFIX}toggle_button {
position: absolute; bottom: 55px; left: 15px;
padding: 5px 8px; background-color: rgba(20, 20, 20, 0.7); color: white;
border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px;
cursor: pointer; z-index: 65; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
font-size: 12px; line-height: 1;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
}
#${SCRIPT_PREFIX}toggle_button:hover {
background-color: rgba(40, 40, 40, 0.9);
}
#movie_player.relative-positioning,
.html5-video-player.relative-positioning,
ytd-player.relative-positioning {
position: relative !important;
}
#${SCRIPT_PREFIX}panel h3, #${SCRIPT_PREFIX}panel h4 { margin-top: 0; margin-bottom: 10px; color: #333; }
#${SCRIPT_PREFIX}panel label { display: block; margin-bottom: 3px; font-weight: bold; color: #555; }
#${SCRIPT_PREFIX}panel div { margin-bottom: 10px; }
#${SCRIPT_PREFIX}panel input[type="password"],
#${SCRIPT_PREFIX}panel select,
#${SCRIPT_PREFIX}panel textarea { padding: 6px; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; width: 100%;}
#${SCRIPT_PREFIX}api_key_input { width: calc(100% - 165px); margin-right: 5px; vertical-align: middle; }
#${SCRIPT_PREFIX}summary_output { width: 100%; background-color: #fff; }
#${SCRIPT_PREFIX}panel button { background-color: #4CAF50; color: white; padding: 8px 12px; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px; vertical-align: middle; }
#${SCRIPT_PREFIX}panel button:last-child { margin-right: 0; }
#${SCRIPT_PREFIX}panel button:hover { background-color: #45a049; }
#${SCRIPT_PREFIX}panel button:disabled { background-color: #ccc; cursor: not-allowed; }
`);
}
function updateStatus(message, isError = false) {
if (statusDiv) {
statusDiv.textContent = message;
statusDiv.style.color = isError ? 'red' : 'green';
}
// console.log(SCRIPT_PREFIX + (isError ? "Error: " : "Status: ") + message);
}
function handleFetchModelsClick() {
const newApiKey = apiKeyInput.value.trim();
if (!newApiKey) {
updateStatus("API Key cannot be empty.", true);
return;
}
apiKey = newApiKey;
saveSettings();
updateStatus("API Key saved. Fetching models...");
fetchAndPopulateModels();
}
function fetchAndPopulateModels() {
if (!apiKey) {
updateStatus("API Key is not set. Cannot fetch models.", true);
populateModelDropdown([]);
return;
}
updateStatus("Fetching models...");
GM_xmlhttpRequest({
method: "GET",
url: `${GEMINI_API_BASE_URL}?key=${apiKey}`,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
models = data.models.filter(model =>
model.supportedGenerationMethods && model.supportedGenerationMethods.includes("generateContent")
);
if (models.length === 0) {
updateStatus("No models supporting 'generateContent' found for this API key.", true);
populateModelDropdown([]);
return;
}
updateStatus(`Found ${models.length} compatible models.`, false);
populateModelDropdown(models);
} catch (e) {
updateStatus("Error parsing models response: " + e.message, true);
console.error(SCRIPT_PREFIX + "Error parsing models response:", e, response.responseText);
populateModelDropdown([]);
}
} else {
let errorMsg = `Error fetching models: ${response.status}`;
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error && errorData.error.message) {
errorMsg += ` - ${errorData.error.message}`;
}
} catch (e) { /* ignore */ }
updateStatus(errorMsg, true);
console.error(SCRIPT_PREFIX + "Error fetching models:", response);
populateModelDropdown([]);
}
updateSummarizeButtonState();
},
onerror: function(response) {
updateStatus("Network error fetching models: " + response.statusText, true);
console.error(SCRIPT_PREFIX + "Network error fetching models:", response);
populateModelDropdown([]);
updateSummarizeButtonState();
}
});
}
function populateModelDropdown(modelsToPopulate) {
while (modelSelect.firstChild) {
modelSelect.removeChild(modelSelect.firstChild);
}
if (modelsToPopulate.length === 0) {
const option = document.createElement('option');
option.value = "";
option.textContent = "No models available";
modelSelect.appendChild(option);
modelSelect.disabled = true;
selectedModel = "";
} else {
modelsToPopulate.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.displayName + ` (${model.name.split('/')[1]})`;
modelSelect.appendChild(option);
});
modelSelect.disabled = false;
let foundPrevious = false;
if (selectedModel) {
for (let i = 0; i < modelSelect.options.length; i++) {
if (modelSelect.options[i].value === selectedModel) {
modelSelect.selectedIndex = i;
foundPrevious = true;
break;
}
}
}
if (!foundPrevious) {
let flashModelIndex = modelsToPopulate.findIndex(m => m.name.includes("flash"));
if (flashModelIndex !== -1) {
modelSelect.selectedIndex = flashModelIndex;
} else if (modelSelect.options.length > 0) {
modelSelect.selectedIndex = 0;
}
}
selectedModel = modelSelect.value;
}
saveSettings();
updateSummarizeButtonState();
}
function updateSummarizeButtonState() {
if (summarizeButton) {
summarizeButton.disabled = !(apiKey && selectedModel && models.length > 0 && modelSelect.value && !modelSelect.disabled && selectedLanguage);
}
}
function setSummaryTextAndDirection(text, languageValue) {
summaryOutput.value = text;
const languageInfo = SUPPORTED_LANGUAGES.find(lang => lang.value === languageValue);
if (languageInfo && languageInfo.isRTL) {
summaryOutput.dir = 'rtl';
summaryOutput.style.textAlign = 'right';
} else {
summaryOutput.dir = 'ltr';
summaryOutput.style.textAlign = 'left';
}
}
async function handleSummarizeClick() {
updateStatus("Starting summarization...", false);
setSummaryTextAndDirection("", selectedLanguage); // Clear previous summary and set initial direction
if (!apiKey || !selectedModel || !modelSelect.value || !selectedLanguage) {
updateStatus("API Key, Model, or Language not selected correctly.", true);
return;
}
const currentSelectedModel = modelSelect.value;
const currentSelectedLanguage = languageSelect.value;
const transcript = await getYouTubeTranscript();
if (!transcript) {
updateStatus("Could not retrieve video transcript.", true);
// summaryOutput is already cleared and set to LTR by setSummaryTextAndDirection above
return;
}
updateStatus(`Transcript retrieved. Sending to Gemini API for summary in ${currentSelectedLanguage}...`, false);
GM_xmlhttpRequest({
method: "POST",
url: `${GEMINI_API_BASE_URL}/${currentSelectedModel.replace("models/", "")}:generateContent?key=${apiKey}`,
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({
"contents": [{
"parts": [{
"text": `Please summarize the following video transcript in ${currentSelectedLanguage}:\n\n${transcript}`
}]
}],
"generationConfig": {
"temperature": 0.7,
"topK": 1,
"topP": 1,
"maxOutputTokens": 2048,
}
}),
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (data.candidates && data.candidates.length > 0 &&
data.candidates[0].content && data.candidates[0].content.parts &&
data.candidates[0].content.parts.length > 0 && data.candidates[0].content.parts[0].text) {
const summaryText = data.candidates[0].content.parts[0].text;
setSummaryTextAndDirection(summaryText, currentSelectedLanguage);
updateStatus("Summary generated successfully!", false);
} else if (data.promptFeedback && data.promptFeedback.blockReason) {
const errorText = `Error: Content generation blocked. Reason: ${data.promptFeedback.blockReasonDetails || data.promptFeedback.blockReason}`;
setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE); // Show error in default LTR language
updateStatus(`Content blocked: ${data.promptFeedback.blockReason}`, true);
} else {
const errorText = "Error: Received an unexpected response format from Gemini.";
setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
updateStatus("Could not extract summary from Gemini response.", true);
console.error(SCRIPT_PREFIX + "Gemini response format error:", data);
}
} catch (e) {
const errorText = "Error: Could not parse the response from Gemini.";
setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
updateStatus("Error parsing Gemini response: " + e.message, true);
console.error(SCRIPT_PREFIX + "Error parsing Gemini response:", e, response.responseText);
}
} else {
let errorMsg = `Error from Gemini API: ${response.status}`;
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error && errorData.error.message) {
errorMsg += ` - ${errorData.error.message}`;
}
} catch (e) { /* ignore */ }
setSummaryTextAndDirection(`Error: Gemini API returned status ${response.status}.`, DEFAULT_LANGUAGE);
updateStatus(errorMsg, true);
console.error(SCRIPT_PREFIX + "Error from Gemini API:", response);
}
},
onerror: function(response) {
const errorText = "Error: Network problem while contacting Gemini API.";
setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
updateStatus("Network error during summarization: " + response.statusText, true);
console.error(SCRIPT_PREFIX + "Network error during summarization:", response);
}
});
}
async function getYouTubeTranscript() {
updateStatus("Attempting to fetch transcript...", false);
try {
// Try to find the three-dots menu or settings button
const threeDotsButton = document.querySelector('button#button.style-scope.ytd-menu-renderer[aria-label="More actions"], button.ytp-button[title="Settings"], button[aria-label*="More actions" i][aria-haspopup="true"]');
if (threeDotsButton) {
threeDotsButton.click();
await new Promise(resolve => setTimeout(resolve, 600)); // Wait for menu to appear
} else {
console.warn(SCRIPT_PREFIX + "Three dots/Settings button not found initially.");
}
// Try to find "Show transcript" or similar button in the menu
let transcriptButton = Array.from(document.querySelectorAll('yt-formatted-string, ytd-menu-service-item-renderer, tp-yt-paper-item, div.ytp-menuitem-label'))
.find(el => {
const text = el.textContent?.trim().toLowerCase();
// More robust matching for different languages and phrasings
return text === 'show transcript' || text === 'показать расшифровку видео' || text === 'расшифровка' || text === 'transkript anzeigen' || text === 'mostrar transcripción';
});
if (transcriptButton) {
// Ensure it's a clickable element within a popup/menu context
if (transcriptButton.closest('ytd-menu-popup-renderer') || transcriptButton.closest('.ytp-popup') || transcriptButton.closest('tp-yt-paper-listbox')) {
transcriptButton.click();
} else {
// If not in a typical popup, try clicking anyway if it's visible.
// This might be the case if YouTube changes structure slightly.
if(transcriptButton.offsetParent !== null) transcriptButton.click();
else console.warn(SCRIPT_PREFIX + "Transcript button found but not in expected parent, or not visible.");
}
} else {
// Fallback: try to find a direct transcript button in description (less common for video watch page)
const directTranscriptButton = document.querySelector('#description-inline-expander ytd-structured-description-content-renderer [aria-label*="transcript"], #description ytd-text-inline-expander [aria-label*="transcript"]');
if(directTranscriptButton) {
directTranscriptButton.click();
} else {
// Check if transcript panel is already open
const existingPanel = document.querySelector("ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript'], ytd-transcript-renderer, .ytd-transcript-segment-list-renderer");
if (!existingPanel) {
updateStatus("Show transcript button/panel not found or could not be opened.", true);
return getTranscriptFromWindow(); // Fallback to window method
} else {
updateStatus("Transcript panel might be already open or button was not found.", false);
}
}
}
updateStatus("Transcript panel requested or already open. Waiting for content...", false);
await new Promise(resolve => setTimeout(resolve, 2500)); // Wait for panel to load
const transcriptPanelSelectors = [
"ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript']",
"ytd-transcript-renderer", // Generic renderer
".ytd-transcript-segment-list-renderer", // Segment list
"div#transcript.ytd-item-section-renderer",
"ytd-transcript-body-renderer"
];
let transcriptPanelElement;
for (const selector of transcriptPanelSelectors) {
transcriptPanelElement = document.querySelector(selector);
if (transcriptPanelElement) break;
}
if (!transcriptPanelElement) {
updateStatus("Transcript panel DOM element not found after click/check.", true);
return getTranscriptFromWindow(); // Fallback
}
const segmentSelectors = [
'ytd-transcript-segment-renderer .segment-text',
'.ytd-transcript-body-renderer .cue-group .cue',
'yt-formatted-string.ytd-transcript-segment-renderer',
'div[role="button"].ytd-transcript-segment-renderer div.text',
'yt-formatted-string.ytd-transcript-cue-group-renderer',
'.cue.ytd-transcript-body-renderer' // Simpler selector for cue text
];
let segments;
for (const selector of segmentSelectors) {
segments = transcriptPanelElement.querySelectorAll(selector);
if (segments && segments.length > 0) break;
}
let fullTranscript = "";
if (segments && segments.length > 0) {
segments.forEach(segment => {
fullTranscript += (segment.textContent || "").trim().replace(/\s+/g, ' ') + " ";
});
}
if (fullTranscript.trim()) {
updateStatus("Transcript extracted from panel.", false);
// Attempt to close transcript panel or menu
try {
const closeButtonSelectors = [
'#engagement-panel-searchable-transcript button[aria-label*="Close" i]', // More generic close
'ytd-engagement-panel-title-header-renderer button[aria-label*="Close" i]',
'button.ytp-settings-button[title="Settings"][aria-expanded="true"]',
'ytd-transcript-renderer #header button[aria-label*="Close" i]',
'ytd-popup-container tp-yt-paper-dialog #header #close-button',
'button[aria-label*="Close transcript" i]' // Case-insensitive
];
let closeButton;
for(const selector of closeButtonSelectors){
closeButton = document.querySelector(selector);
if(closeButton && closeButton.offsetParent !== null) break;
else closeButton = null;
}
if (closeButton) {
// console.log(SCRIPT_PREFIX + "Attempting to close transcript panel with button:", closeButton);
closeButton.click();
} else if (threeDotsButton && threeDotsButton.getAttribute('aria-expanded') === 'true') {
// console.log(SCRIPT_PREFIX + "Attempting to close three-dots menu (fallback).");
threeDotsButton.click();
}
} catch(e) { console.warn(SCRIPT_PREFIX + "Could not close transcript panel/menu cleanly:", e); }
return fullTranscript.trim();
} else {
updateStatus("Transcript panel/segments found, but no text extracted.", true);
return getTranscriptFromWindow(); // Fallback
}
} catch (error) {
updateStatus("Error interacting with transcript UI: " + error.message, true);
console.error(SCRIPT_PREFIX + "Error in getYouTubeTranscript (UI interaction):", error);
return getTranscriptFromWindow(); // Fallback on any UI error
}
}
function getTranscriptFromWindow() {
updateStatus("Attempting fallback transcript extraction (window.ytInitialPlayerResponse)...", false);
try {
const playerResponse = window.ytInitialPlayerResponse || (window.ytplayer && window.ytplayer.config && window.ytplayer.config.args && window.ytplayer.config.args.player_response && JSON.parse(window.ytplayer.config.args.player_response));
if (playerResponse && playerResponse.captions &&
playerResponse.captions.playerCaptionsTracklistRenderer) {
const tracks = playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks;
if (tracks && tracks.length > 0) {
let chosenTrack = tracks.find(t => t.kind === 'asr' && (t.languageCode === 'en' || t.languageCode.startsWith('en-'))) ||
tracks.find(t => t.kind === 'asr') ||
tracks.find(t => (t.languageCode === 'en' || t.languageCode.startsWith('en-'))) ||
tracks[0];
if (chosenTrack && chosenTrack.baseUrl) {
updateStatus(`Found caption track: ${chosenTrack.name ? (chosenTrack.name.simpleText || JSON.stringify(chosenTrack.name)) : 'Unknown'} (${chosenTrack.languageCode}). Fetching...`, false);
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: chosenTrack.baseUrl + "&fmt=json3",
onload: function(response) {
if (response.status === 200) {
try {
const captionData = JSON.parse(response.responseText);
let transcriptText = "";
if (captionData.events) {
captionData.events.forEach(event => {
if (event.segs) {
event.segs.forEach(seg => {
transcriptText += seg.utf8.replace(/\n/g, ' ').trim() + " ";
});
}
});
}
if (transcriptText.trim()) {
updateStatus("Transcript extracted from caption track.", false);
resolve(transcriptText.trim());
} else {
updateStatus("Caption track fetched but no text found.", true);
resolve(null);
}
} catch (e) {
updateStatus("Error parsing caption data: " + e.message, true);
console.error(SCRIPT_PREFIX + "Error parsing caption data:", e);
resolve(null);
}
} else {
updateStatus(`Error fetching caption track: ${response.status}`, true);
resolve(null);
}
},
onerror: function() {
updateStatus("Network error fetching caption track.", true);
resolve(null);
}
});
});
}
}
}
updateStatus("No suitable caption tracks found in player response.", true);
return Promise.resolve(null);
} catch (e) {
updateStatus("Error accessing player response for transcripts: " + e.message, true);
console.error(SCRIPT_PREFIX + "Error in getTranscriptFromWindow:", e);
return Promise.resolve(null);
}
}
function runDiagnosticChecks() {
// Diagnostics can be verbose, enable if needed by uncommenting console.logs
if (!playerContainer || !togglePanelButton) {
return;
}
// const currentMoviePlayer = document.getElementById('movie_player');
// console.log(SCRIPT_PREFIX + "DIAG --- Interval Check ---");
// console.log(SCRIPT_PREFIX + `DIAG: playerContainer.isConnected: ${playerContainer.isConnected}`);
// console.log(SCRIPT_PREFIX + `DIAG: current #movie_player === initial playerContainer: ${currentMoviePlayer === playerContainer}`);
// if(currentMoviePlayer !== playerContainer && currentMoviePlayer) {
// console.warn(SCRIPT_PREFIX + `DIAG: #movie_player element seems to have been REPLACED!`);
// }
// try {
// const rect = playerContainer.getBoundingClientRect();
// console.log(SCRIPT_PREFIX + `DIAG: playerContainer rect (isActive: ${playerContainer.isConnected}): w=${rect.width.toFixed(0)}, h=${rect.height.toFixed(0)}, top=${rect.top.toFixed(0)}, left=${rect.left.toFixed(0)}`);
// } catch (e) { /* ignore if disconnected */ }
// console.log(SCRIPT_PREFIX + `DIAG: toggleButton opacity: ${togglePanelButton.style.opacity}, pointerEvents: ${togglePanelButton.style.pointerEvents}`);
// console.log(SCRIPT_PREFIX + "DIAG --- End Interval Check ---");
}
function handleGlobalMouseOver(event) {
if (playerContainer && (event.target === playerContainer || playerContainer.contains(event.target))) {
showToggleButton();
startHideButtonTimer();
}
}
function handleGlobalMouseOut(event) {
if (playerContainer && (event.target === playerContainer || playerContainer.contains(event.target))) {
const isLeavingToActiveElements = (panel && panel.contains(event.relatedTarget)) ||
(togglePanelButton && togglePanelButton.contains(event.relatedTarget)) ||
(playerContainer && playerContainer.contains(event.relatedTarget));
if (!isLeavingToActiveElements) {
startHideButtonTimer(HIDE_BUTTON_DELAY_FAST);
}
}
}
function init() {
console.log(SCRIPT_PREFIX + "Attempting to initialize script...");
loadSettings();
document.body.addEventListener('mouseover', handleGlobalMouseOver, true);
document.body.addEventListener('mouseout', handleGlobalMouseOut, true);
// console.log(SCRIPT_PREFIX + "Global mouseover/mouseout listeners ADDED to document.body (capture).");
const playerCheckInterval = setInterval(function() {
playerContainer = document.getElementById('movie_player') ||
document.querySelector('.html5-video-player') ||
document.querySelector('ytd-player');
if (playerContainer && (document.readyState === 'interactive' || document.readyState === 'complete')) {
clearInterval(playerCheckInterval);
console.log(SCRIPT_PREFIX + "Player container found.");
addStyles();
createGUI();
loadSettings(); // Ensure selects are populated correctly
createToggleButton();
if (togglePanelButton && playerContainer) {
const currentPosition = window.getComputedStyle(playerContainer).position;
if (currentPosition === 'static' || !currentPosition) {
playerContainer.classList.add('relative-positioning');
}
playerContainer.appendChild(togglePanelButton);
// console.log(SCRIPT_PREFIX + "Toggle button appended.");
const buttonPanelMouseOverHandler = (event) => {
showToggleButton();
event.stopPropagation();
};
togglePanelButton.addEventListener('mouseover', buttonPanelMouseOverHandler);
if (panel) panel.addEventListener('mouseover', buttonPanelMouseOverHandler);
const buttonPanelMouseLeaveHandler = (event) => {
if (playerContainer && !playerContainer.contains(event.relatedTarget) &&
togglePanelButton && !togglePanelButton.contains(event.relatedTarget) &&
panel && !panel.contains(event.relatedTarget)) {
startHideButtonTimer(HIDE_BUTTON_DELAY_FAST);
}
};
togglePanelButton.addEventListener('mouseleave', buttonPanelMouseLeaveHandler);
if (panel) panel.addEventListener('mouseleave', buttonPanelMouseLeaveHandler);
} else {
console.error(SCRIPT_PREFIX + "togglePanelButton or playerContainer not available for event setup.");
}
if (apiKeyInput && apiKey) {
apiKeyInput.value = apiKey;
fetchAndPopulateModels();
} else {
populateModelDropdown([]);
}
updateStatus("Summarizer ready. " + (isPanelVisible ? "Panel is visible." : ""), false);
if (diagnosticIntervalId) clearInterval(diagnosticIntervalId);
diagnosticIntervalId = setInterval(runDiagnosticChecks, DIAGNOSTIC_INTERVAL_MS);
}
}, 1000);
setTimeout(() => {
if (!document.getElementById(SCRIPT_PREFIX + 'panel')) {
clearInterval(playerCheckInterval);
if (diagnosticIntervalId) clearInterval(diagnosticIntervalId);
document.body.removeEventListener('mouseover', handleGlobalMouseOver, true);
document.body.removeEventListener('mouseout', handleGlobalMouseOut, true);
console.warn(SCRIPT_PREFIX + "Fallback init triggered. Main init likely failed.");
playerContainer = document.getElementById('movie_player') || document.querySelector('.html5-video-player') || document.querySelector('ytd-player');
if (playerContainer && !document.getElementById(SCRIPT_PREFIX + 'toggle_button')) {
console.log(SCRIPT_PREFIX + "FALLBACK: Player found, attempting to setup GUI.");
addStyles(); createGUI(); loadSettings(); createToggleButton();
if (togglePanelButton) {
const currentPosition = window.getComputedStyle(playerContainer).position;
if (currentPosition === 'static' || !currentPosition) playerContainer.classList.add('relative-positioning');
playerContainer.appendChild(togglePanelButton);
playerContainer.addEventListener('mouseover', () => { showToggleButton(); startHideButtonTimer(); }, false);
playerContainer.addEventListener('mouseout', () => { startHideButtonTimer(HIDE_BUTTON_DELAY_FAST); }, false);
togglePanelButton.addEventListener('mouseover', () => { showToggleButton(); });
if(panel) panel.addEventListener('mouseover', () => { showToggleButton(); });
if(isPanelVisible) showToggleButton(); else hideToggleButton();
}
if (apiKeyInput && apiKey) { apiKeyInput.value = apiKey; fetchAndPopulateModels(); }
else { populateModelDropdown([]); }
updateStatus("Summarizer ready (Fallback Init). " + (isPanelVisible ? "Panel is visible." : ""), false);
} else if (!playerContainer) {
console.error(SCRIPT_PREFIX + "FALLBACK: Player container NOT FOUND.");
}
}
}, 15000);
}
function main() {
init();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
main();
} else {
window.addEventListener('DOMContentLoaded', main, { once: true }); // Changed to DOMContentLoaded for slightly earlier execution
}
})();