diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-04-18 17:36:52 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-04-18 17:36:52 -0700 |
| commit | 85a1c3df4b0f04f94c2d0179c21eeda36f9a9e3c (patch) | |
| tree | 6c093e9cca4bbb1a9e5bdfe83056e29e12f8dbdf /youtube-subtitle.user.js | |
| parent | e6700aa1cd867fc574eb8e6df75b26eae269b759 (diff) | |
init commit
Diffstat (limited to 'youtube-subtitle.user.js')
| -rw-r--r-- | youtube-subtitle.user.js | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/youtube-subtitle.user.js b/youtube-subtitle.user.js new file mode 100644 index 0000000..a860981 --- /dev/null +++ b/youtube-subtitle.user.js @@ -0,0 +1,234 @@ +// ==UserScript== +// @name Auto-Live-TL YouTube Client +// @namespace https://example.com/ +// @version 1.0 +// @description Auto Translate Live Subtitles +// @author pinapelz +// @match https://www.youtube.com/* +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function () { + "use strict"; + + const PANEL_ID = "altl-subtitle-panel"; + const SUBTITLE_TEXT_ID = "altl-subtitle-text"; + const FOOTER_TEXT_ID = "altl-footer-text"; + const EVENTS_URL = "http://127.0.0.1:5000/events"; + + let eventSource = null; + let isConnected = false; + let lastError = null; + let reconnectTimer = null; + const MAX_SUBTITLE_LINES = 3; + const recentLines = []; + + function getPrimary() { + return document.querySelector("#columns #primary"); + } + + function getPlayerNode(primary) { + return ( + primary?.querySelector("#player") || + primary?.querySelector("ytd-player") || + primary?.querySelector("#movie_player")?.closest("#player") || + primary?.querySelector("#movie_player") || + null + ); + } + + function getTitleAnchor(primary) { + return ( + primary?.querySelector("ytd-watch-metadata") || + primary?.querySelector("#title h1")?.closest("ytd-watch-metadata") || + primary?.querySelector("#below ytd-watch-metadata") || + primary?.querySelector("#below")?.firstElementChild || + null + ); + } + + function ensurePanel() { + let panel = document.getElementById(PANEL_ID); + if (!panel) { + panel = document.createElement("div"); + panel.id = PANEL_ID; + + const subtitleText = document.createElement("div"); + subtitleText.id = SUBTITLE_TEXT_ID; + subtitleText.textContent = "This is a sample subtitle."; + + const footerText = document.createElement("div"); + footerText.id = FOOTER_TEXT_ID; + footerText.textContent = "Machine Translated - no translation should be taken as authoritative or quoted verbatim"; + footerText.style.marginTop = "8px"; + footerText.style.fontSize = "12px"; + footerText.style.opacity = "0.7"; + footerText.style.textAlign = "center"; + + panel.appendChild(subtitleText); + panel.appendChild(footerText); + + panel.style.display = "block"; + panel.style.boxSizing = "border-box"; + panel.style.width = "100%"; + panel.style.margin = "8px 0 12px"; + panel.style.padding = "10px 14px"; + panel.style.borderRadius = "10px"; + panel.style.background = "rgba(255,255,255,0.04)"; + panel.style.border = "1px solid rgba(255,255,255,0.12)"; + panel.style.color = "var(--yt-spec-text-primary, #f1f1f1)"; + panel.style.fontSize = "16px"; + panel.style.lineHeight = "1.45"; + panel.style.fontWeight = "500"; + panel.style.textAlign = "center"; + panel.style.whiteSpace = "pre-wrap"; + panel.style.wordBreak = "break-word"; + } + return panel; + } + + function getSubtitleNode(panel) { + return panel?.querySelector(`#${SUBTITLE_TEXT_ID}`) || null; + } + + + + function setSubtitleText(panel, text) { + const node = getSubtitleNode(panel); + if (node) node.textContent = text; + } + + + + function renderSubtitleHistory(panel) { + const node = getSubtitleNode(panel); + if (!node) return; + node.textContent = ""; + recentLines.forEach((line, index) => { + const row = document.createElement("div"); + row.textContent = line; + const opacity = Math.max(0.35, 1 - index * 0.15); + row.style.opacity = String(opacity); + if (index > 0) row.style.filter = "grayscale(1)"; + node.appendChild(row); + }); + } + + function updateSubtitleHistory(panel, text) { + if (!text) return; + if (recentLines.length > 0 && recentLines[0] === text) { + renderSubtitleHistory(panel); + return; + } + recentLines.unshift(text); + if (recentLines.length > MAX_SUBTITLE_LINES) { + recentLines.splice(MAX_SUBTITLE_LINES); + } + renderSubtitleHistory(panel); + } + + function safePlaceBefore(targetNode, nodeToPlace) { + if (!targetNode || !targetNode.parentNode || !targetNode.isConnected) return false; + + const parent = targetNode.parentNode; + if (nodeToPlace.parentNode !== parent || nodeToPlace.nextSibling !== targetNode) { + parent.insertBefore(nodeToPlace, targetNode); + } + return true; + } + + function safePlaceAfter(targetNode, nodeToPlace) { + if (!targetNode || !targetNode.parentNode || !targetNode.isConnected) return false; + + const parent = targetNode.parentNode; + const desiredNext = targetNode.nextSibling; + if (nodeToPlace.parentNode !== parent || nodeToPlace.previousSibling !== targetNode) { + parent.insertBefore(nodeToPlace, desiredNext); + } + return true; + } + + function connectEventSource(panel) { + if (eventSource) { + eventSource.close(); + eventSource = null; + } + + setSubtitleText(panel, "Connecting to local subtitle server..."); + const source = new EventSource(EVENTS_URL); + eventSource = source; + + source.addEventListener("subtitle", (event) => { + try { + const data = JSON.parse(event.data || "{}"); + updateSubtitleHistory(panel, data.text || ""); + } catch (err) { + lastError = err; + console.warn("Auto-Live-TL: failed to parse subtitle event", err); + } + }); + + + + source.onopen = () => { + isConnected = true; + if (recentLines.length > 0) { + renderSubtitleHistory(panel); + } else { + setSubtitleText(panel, "Connected. Waiting for subtitles..."); + } + }; + + source.onerror = () => { + isConnected = false; + setSubtitleText(panel, "Connection lost. Reconnecting..."); + if (eventSource) { + eventSource.close(); + eventSource = null; + } + if (!reconnectTimer) { + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connectEventSource(panel); + }, 2000); + } + }; + } + + function startListeningIfNeeded(panel) { + if (isConnected || eventSource) return; + if (!location.pathname.startsWith("/watch")) return; + connectEventSource(panel); + } + + function injectSubtitlePanel() { + if (!location.pathname.startsWith("/watch")) return false; + + const primary = getPrimary(); + if (!primary) return false; + + const panel = ensurePanel(); + + startListeningIfNeeded(panel); + const titleAnchor = getTitleAnchor(primary); + if (safePlaceBefore(titleAnchor, panel)) return true; + const playerNode = getPlayerNode(primary); + if (safePlaceAfter(playerNode, panel)) return true; + + return false; + } + + injectSubtitlePanel(); + + const observer = new MutationObserver(() => { + injectSubtitlePanel(); + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); + + window.addEventListener("yt-navigate-finish", injectSubtitlePanel); +})(); |
