3f1da92cf4
YouTube subtitles via OpenRouter become persistent, shareable recaps with timestamp jump links, PIN-protected creation, and Traefik deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
import {
|
|
api,
|
|
VERDICT_LABELS,
|
|
formatTimestamp,
|
|
youtubeUrl,
|
|
getToken,
|
|
setToken,
|
|
isLoggedIn,
|
|
} from "./api.js";
|
|
|
|
function getVideoIdFromPath() {
|
|
const parts = window.location.pathname.split("/").filter(Boolean);
|
|
if (parts[0] === "r" && parts[1]) return decodeURIComponent(parts[1]);
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get("v") || "";
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function setMeta(recap) {
|
|
document.title = `${recap.title} — Goldfish Recap`;
|
|
document.getElementById("metaDescription").content = recap.tldr;
|
|
document.getElementById("ogTitle").content = recap.title;
|
|
document.getElementById("ogDescription").content = recap.tldr;
|
|
document.getElementById("ogImage").content = recap.thumbnail_url || "";
|
|
}
|
|
|
|
function renderRecap(recap) {
|
|
const verdict = VERDICT_LABELS[recap.watch_verdict] || VERDICT_LABELS.skim;
|
|
document.getElementById("verdictBadge").className = `verdictBadge ${verdict.className}`;
|
|
document.getElementById("verdictBadge").textContent = `${verdict.emoji} ${verdict.text}`;
|
|
document.getElementById("videoTitle").textContent = recap.title;
|
|
document.getElementById("channelName").textContent = recap.channel || "";
|
|
document.getElementById("thumbImg").src = recap.thumbnail_url || "";
|
|
document.getElementById("thumbImg").alt = recap.title;
|
|
document.getElementById("thumbLink").href = youtubeUrl(recap.video_id);
|
|
document.getElementById("tldrText").textContent = recap.tldr;
|
|
document.getElementById("vibeText").textContent = recap.vibe_check || "";
|
|
document.getElementById("goldfishNote").textContent = recap.goldfish_note || "";
|
|
|
|
const sections = recap.sections || [];
|
|
document.getElementById("sectionsList").innerHTML = sections
|
|
.map(
|
|
(s) => `
|
|
<a class="sectionCard" href="${youtubeUrl(recap.video_id, s.timestamp_seconds)}" target="_blank" rel="noopener">
|
|
<span class="sectionTime">${formatTimestamp(s.timestamp_seconds)}</span>
|
|
<span class="sectionEmoji">${escapeHtml(s.emoji || "🐟")}</span>
|
|
<div>
|
|
<h3>${escapeHtml(s.title)}</h3>
|
|
<p>${escapeHtml(s.summary)}</p>
|
|
</div>
|
|
</a>
|
|
`
|
|
)
|
|
.join("");
|
|
|
|
setMeta(recap);
|
|
}
|
|
|
|
function showError(msg) {
|
|
document.getElementById("loadingState").hidden = true;
|
|
document.getElementById("recapMain").hidden = true;
|
|
document.getElementById("errorState").hidden = false;
|
|
document.getElementById("errorText").textContent = msg;
|
|
}
|
|
|
|
async function loadRecap() {
|
|
const videoId = getVideoIdFromPath();
|
|
if (!videoId) {
|
|
showError("Keine Video-ID in der URL.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const recap = await api.getRecap(videoId);
|
|
document.getElementById("loadingState").hidden = true;
|
|
document.getElementById("recapMain").hidden = false;
|
|
renderRecap(recap);
|
|
|
|
if (isLoggedIn()) {
|
|
document.getElementById("adminRow").hidden = false;
|
|
}
|
|
|
|
document.getElementById("copyLinkBtn").addEventListener("click", async () => {
|
|
const url = `${window.location.origin}/r/${encodeURIComponent(videoId)}`;
|
|
try {
|
|
await navigator.clipboard.writeText(url);
|
|
document.getElementById("copyLinkBtn").textContent = "Kopiert!";
|
|
setTimeout(() => {
|
|
document.getElementById("copyLinkBtn").textContent = "Link kopieren";
|
|
}, 2000);
|
|
} catch {
|
|
window.prompt("Link kopieren:", url);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
showError(err.message || "Recap nicht gefunden.");
|
|
}
|
|
}
|
|
|
|
function wireRegenerate() {
|
|
document.getElementById("regenerateBtn").addEventListener("click", async () => {
|
|
const videoId = getVideoIdFromPath();
|
|
const btn = document.getElementById("regenerateBtn");
|
|
btn.disabled = true;
|
|
btn.textContent = "Generiere…";
|
|
try {
|
|
const result = await api.regenerateRecap(videoId);
|
|
renderRecap(result.recap);
|
|
} catch (err) {
|
|
alert(err.message || "Regenerieren fehlgeschlagen.");
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = "Neu generieren";
|
|
}
|
|
});
|
|
}
|
|
|
|
function wirePinModal() {
|
|
const pinModal = document.getElementById("pinModal");
|
|
document.getElementById("pinCancel").addEventListener("click", () => pinModal.close());
|
|
document.getElementById("pinForm").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const errEl = document.getElementById("pinError");
|
|
errEl.hidden = true;
|
|
try {
|
|
const { token } = await api.login(document.getElementById("pinInput").value.trim());
|
|
setToken(token);
|
|
pinModal.close();
|
|
document.getElementById("adminRow").hidden = false;
|
|
} catch {
|
|
errEl.textContent = "Falscher PIN.";
|
|
errEl.hidden = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
wireRegenerate();
|
|
wirePinModal();
|
|
loadRecap();
|
|
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
}
|