aboutsummaryrefslogtreecommitdiffstats
path: root/youtube-subtitle.user.js
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-04-18 17:36:52 -0700
committerPinapelz <yukais@pinapelz.com>2026-04-18 17:36:52 -0700
commit85a1c3df4b0f04f94c2d0179c21eeda36f9a9e3c (patch)
tree6c093e9cca4bbb1a9e5bdfe83056e29e12f8dbdf /youtube-subtitle.user.js
parente6700aa1cd867fc574eb8e6df75b26eae269b759 (diff)
init commit
Diffstat (limited to 'youtube-subtitle.user.js')
-rw-r--r--youtube-subtitle.user.js234
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);
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage