finance-tools/extension/content-youtube.js

191 lines
5.8 KiB
JavaScript
Raw Normal View History

2026-06-22 09:16:20 +00:00
// Investor RPG — YouTube 內容擷取
// 執行在 YouTube 頁面上,利用使用者的登入 session 取得字幕
const API_HOST = "http://localhost:3000";
let injectButton = null;
function extractVideoId() {
const m = window.location.pathname.match(/\/watch\/([a-zA-Z0-9_-]{11})/) ||
window.location.search.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
return m ? m[1] : null;
}
function extractPlaylistId() {
const m = window.location.search.match(/[?&]list=([a-zA-Z0-9_-]+)/);
return m ? m[1] : null;
}
function getVideoTitle() {
const el = document.querySelector("h1 yt-formatted-string") ||
document.querySelector("#title h1") ||
document.querySelector("meta[itemprop='name']");
return el?.textContent?.trim() || el?.getAttribute("content") || document.title.replace(" - YouTube", "");
}
function getChannelName() {
const el = document.querySelector("#owner #channel-name a") ||
document.querySelector("ytd-channel-name a") ||
document.querySelector("link[itemprop='name'][href^='http']");
return el?.textContent?.trim() || el?.getAttribute("content") || "";
}
function getUploadDate() {
const el = document.querySelector("meta[itemprop='datePublished']") ||
document.querySelector("#info-strings yt-formatted-string");
return el?.getAttribute("content") || el?.textContent?.trim() || "";
}
function getThumbnail() {
const el = document.querySelector("meta[property='og:image']");
return el?.getAttribute("content") || "";
}
function injectCaptureButton() {
if (injectButton || !extractVideoId()) return;
injectButton = document.createElement("button");
injectButton.textContent = "📥 擷取到 RPG";
injectButton.style.cssText = `
background: #2a6c3b; color: #fff; border: none; border-radius: 18px;
padding: 6px 16px; font-size: 13px; cursor: pointer;
margin-left: 8px; white-space: nowrap; font-weight: 500;
transition: background .15s;
`;
injectButton.onmouseenter = () => injectButton.style.background = "#1e7e34";
injectButton.onmouseleave = () => injectButton.style.background = "#2a6c3b";
injectButton.onclick = async () => {
injectButton.textContent = "擷取中...";
injectButton.disabled = true;
try {
const result = await captureVideo();
injectButton.textContent = `${result.count || "已送出"}`;
setTimeout(() => {
injectButton.textContent = "📥 擷取到 RPG";
injectButton.disabled = false;
}, 3000);
} catch (err) {
injectButton.textContent = `${err.message.slice(0, 20)}`;
injectButton.style.background = "#a13a3a";
setTimeout(() => {
injectButton.textContent = "📥 擷取到 RPG";
injectButton.style.background = "#2a6c3b";
injectButton.disabled = false;
}, 3000);
}
};
const waitForTarget = () => {
const target = document.querySelector("#menu-container") ||
document.querySelector("#top-level-buttons-computed") ||
document.querySelector("#actions-inner");
if (target) {
target.appendChild(injectButton);
} else {
setTimeout(waitForTarget, 500);
}
};
waitForTarget();
}
async function fetchTranscriptFromPage() {
const videoId = extractVideoId();
if (!videoId) return "";
try {
const apiKey = "";
const response = await fetch(`https://youtubetranscript.com/api?vid=${videoId}`, {
signal: AbortSignal.timeout(8000),
});
if (response.ok) {
const data = await response.json();
return (data.segments || []).map(s => s.text).join(" ");
}
} catch {}
try {
const pageResponse = await fetch(`https://www.youtube.com/watch?v=${videoId}`, {
credentials: "include",
signal: AbortSignal.timeout(8000),
});
const html = await pageResponse.text();
const capsMatch = html.match(/"captionTracks":\s*(\[.*?\])/);
if (capsMatch) {
const tracks = JSON.parse(capsMatch[1]);
const zhTrack = tracks.find(t => t.languageCode?.startsWith("zh")) || tracks[0];
if (zhTrack?.baseUrl) {
const subResp = await fetch(zhTrack.baseUrl, { signal: AbortSignal.timeout(8000) });
const subText = await subResp.text();
return subText.replace(/<\s*\/?\s*[^>]+>/g, "").replace(/[\d:,.\s]+-->[\d:,.\s]+/g, "").replace(/WEBVTT.*?\n/, "").trim();
}
}
} catch {}
return "";
}
function extractDescription() {
const el = document.querySelector("#description yt-formatted-string") ||
document.querySelector("#description");
return el?.textContent?.trim()?.slice(0, 1000) || "";
}
async function captureVideo() {
const videoId = extractVideoId();
if (!videoId) throw new Error("找不到影片 ID");
const title = getVideoTitle();
const channel = getChannelName();
const uploadDate = getUploadDate();
const thumbnail = getThumbnail();
const description = extractDescription();
const transcript = await fetchTranscriptFromPage();
const payload = {
kind: "youtube",
title,
url: window.location.href,
videoId,
transcript,
metadata: {
channel,
uploadDate,
thumbnail,
description,
title,
videoId,
},
};
const res = await fetch(`${API_HOST}/api/content/capture`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `伺服器回應 ${res.status}`);
}
return await res.json();
}
let lastUrl = "";
function checkForVideoChange() {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
if (injectButton) {
injectButton.remove();
injectButton = null;
}
if (extractVideoId()) {
setTimeout(injectCaptureButton, 1000);
}
}
}
setInterval(checkForVideoChange, 1500);
setTimeout(injectCaptureButton, 1500);