// 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);