This commit is contained in:
@@ -88,6 +88,10 @@ func RegisterRoutes(router *gin.Engine, app *App) {
|
||||
router.POST("/api/search", app.searchMedia)
|
||||
}
|
||||
|
||||
func (a *App) debug(message string, data any) {
|
||||
a.Hub.Broadcast("debug", gin.H{"message": message, "data": data})
|
||||
}
|
||||
|
||||
func (a *App) handleWS(c *gin.Context) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
@@ -193,23 +197,28 @@ func (a *App) previewDownload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
a.debug("download preview requested", gin.H{"url": req.URL})
|
||||
cmd := exec.Command("python3", a.WorkerScript, "--mode", "probe", "--url", req.URL)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
a.debug("download preview failed", gin.H{"url": req.URL, "output": string(output), "error": err.Error()})
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": summarizeOutput("download preview probe failed", output, err)})
|
||||
return
|
||||
}
|
||||
|
||||
var preview PreviewResponse
|
||||
if err := json.Unmarshal(output, &preview); err != nil {
|
||||
a.debug("download preview invalid json", gin.H{"url": req.URL, "output": string(output)})
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": summarizeOutput("download preview returned invalid JSON", output, err)})
|
||||
return
|
||||
}
|
||||
a.debug("download preview succeeded", preview)
|
||||
c.JSON(http.StatusOK, preview)
|
||||
}
|
||||
|
||||
func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath string) {
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "queued", "progress": 0, "url": url})
|
||||
a.debug("download command started", gin.H{"url": url, "start": start, "end": end, "quality": quality, "outputPath": outputPath})
|
||||
cmd := exec.Command("python3", a.WorkerScript, "--url", url, "--start", start, "--end", end, "--quality", quality, "--output", outputPath)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
@@ -231,6 +240,7 @@ func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath s
|
||||
var msg map[string]any
|
||||
if err := json.Unmarshal(line, &msg); err == nil {
|
||||
msg["type"] = "download"
|
||||
a.debug("download worker event", msg)
|
||||
a.Hub.Broadcast("progress", msg)
|
||||
}
|
||||
}
|
||||
@@ -242,7 +252,9 @@ func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath s
|
||||
if err := cmd.Wait(); err != nil {
|
||||
status = "failed"
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "error", "progress": 100, "message": err.Error()})
|
||||
a.debug("download command failed", gin.H{"url": url, "error": err.Error()})
|
||||
}
|
||||
a.debug("download command completed", gin.H{"url": url, "status": status, "outputPath": outputPath})
|
||||
_ = models.MarkDownloadCompleted(a.DB, recordID, status)
|
||||
}
|
||||
|
||||
@@ -265,15 +277,18 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
if len(queryVariants) == 0 {
|
||||
queryVariants = []string{req.Query}
|
||||
}
|
||||
a.debug("search query variants", gin.H{"query": req.Query, "variants": queryVariants, "platforms": req.Platforms})
|
||||
|
||||
enabledPlatforms := normalizePlatforms(req.Platforms)
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "searching " + selectedPlatformLabel(enabledPlatforms), "progress": 35})
|
||||
results, err := a.SearchService.SearchMedia(queryVariants, enabledPlatforms)
|
||||
if err != nil {
|
||||
a.debug("search backend failed", gin.H{"error": err.Error(), "variants": queryVariants})
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search failed", "progress": 100, "message": err.Error()})
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
a.debug("search backend results", gin.H{"count": len(results), "results": results})
|
||||
if len(results) == 0 {
|
||||
warning := "SearXNG returned no renderable results."
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "no renderable search results", "progress": 100, "message": warning})
|
||||
@@ -287,8 +302,10 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
rankQuery = strings.Join(queryVariants[:min(len(queryVariants), 3)], " ")
|
||||
}
|
||||
scored := rankSearchResults(rankQuery, results)
|
||||
a.debug("search ranked results", gin.H{"count": len(scored), "results": scored})
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
||||
recommended := evaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||
a.debug("search gemini recommendations", gin.H{"count": len(recommended), "results": recommended})
|
||||
err = nil
|
||||
if len(recommended) == 0 {
|
||||
err = fmt.Errorf("gemini vision returned no recommended items across all candidate batches")
|
||||
|
||||
+122
@@ -30,6 +30,13 @@ const startLabel = document.getElementById("startLabel");
|
||||
const endLabel = document.getElementById("endLabel");
|
||||
const setStartFromPreview = document.getElementById("setStartFromPreview");
|
||||
const setEndFromPreview = document.getElementById("setEndFromPreview");
|
||||
const debugToggle = document.getElementById("debugToggle");
|
||||
const debugPanel = document.getElementById("debugPanel");
|
||||
const closeDebugPanel = document.getElementById("closeDebugPanel");
|
||||
const clearLogs = document.getElementById("clearLogs");
|
||||
const downloadLogs = document.getElementById("downloadLogs");
|
||||
const debugLogList = document.getElementById("debugLogList");
|
||||
const debugSummary = document.getElementById("debugSummary");
|
||||
|
||||
let pendingDownload = null;
|
||||
let cropStart = 0;
|
||||
@@ -38,10 +45,50 @@ let cropMax = 0;
|
||||
let activeThumb = null;
|
||||
const activePlatforms = new Set(["envato", "artgrid", "google video"]);
|
||||
const hlsInstances = new WeakMap();
|
||||
const debugEntries = [];
|
||||
|
||||
function setStatus(label, progress) {
|
||||
statusLabel.textContent = label;
|
||||
statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
||||
logEvent("status", { label, progress });
|
||||
}
|
||||
|
||||
function logEvent(type, payload) {
|
||||
const entry = {
|
||||
ts: new Date().toISOString(),
|
||||
type,
|
||||
payload,
|
||||
};
|
||||
debugEntries.push(entry);
|
||||
if (debugEntries.length > 400) {
|
||||
debugEntries.shift();
|
||||
}
|
||||
renderLogs();
|
||||
}
|
||||
|
||||
function safeStringify(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
debugSummary.textContent = `${debugEntries.length} events captured`;
|
||||
debugLogList.innerHTML = "";
|
||||
for (const entry of debugEntries.slice().reverse()) {
|
||||
const node = document.createElement("div");
|
||||
node.className = "debug-entry";
|
||||
node.innerHTML = `
|
||||
<div class="debug-entry__meta">
|
||||
<span>${entry.type}</span>
|
||||
<span>${entry.ts}</span>
|
||||
</div>
|
||||
<div class="debug-entry__payload">${safeStringify(entry.payload)}</div>
|
||||
`;
|
||||
debugLogList.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
function toClock(totalSeconds) {
|
||||
@@ -101,6 +148,7 @@ function syncPlatformButtons() {
|
||||
button.classList.toggle("text-zinc-300", !active);
|
||||
button.classList.toggle("border-white/20", !active);
|
||||
}
|
||||
logEvent("platforms:update", { active: Array.from(activePlatforms) });
|
||||
}
|
||||
|
||||
function connectWS() {
|
||||
@@ -109,8 +157,10 @@ function connectWS() {
|
||||
socket.addEventListener("message", (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (payload.event !== "progress") {
|
||||
logEvent("ws:message:ignored", payload);
|
||||
return;
|
||||
}
|
||||
logEvent("ws:message", payload);
|
||||
const data = payload.data;
|
||||
const label = data.status || `${data.type || "task"} in progress`;
|
||||
setStatus(label, Number(data.progress ?? 0));
|
||||
@@ -125,13 +175,32 @@ function connectWS() {
|
||||
}
|
||||
});
|
||||
socket.addEventListener("close", () => {
|
||||
logEvent("ws:close", { reason: "socket closed" });
|
||||
setTimeout(connectWS, 1000);
|
||||
});
|
||||
socket.addEventListener("open", () => {
|
||||
logEvent("ws:open", { url: socket.url });
|
||||
});
|
||||
socket.addEventListener("error", () => {
|
||||
logEvent("ws:error", { url: socket.url });
|
||||
});
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
logEvent("api:request", {
|
||||
path,
|
||||
method: options.method || "GET",
|
||||
hasBody: Boolean(options.body),
|
||||
bodyPreview: typeof options.body === "string" ? options.body.slice(0, 800) : "[non-string body]",
|
||||
});
|
||||
const response = await fetch(path, options);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
logEvent("api:response", {
|
||||
path,
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
body: data,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.error || "request failed");
|
||||
error.status = response.status;
|
||||
@@ -144,6 +213,7 @@ async function api(path, options = {}) {
|
||||
function attachVideoSource(video, src) {
|
||||
detachVideoSource(video);
|
||||
if (!src) {
|
||||
logEvent("preview:attach:skipped", { reason: "empty src" });
|
||||
return;
|
||||
}
|
||||
if (src.endsWith(".m3u8") && window.Hls && window.Hls.isSupported()) {
|
||||
@@ -151,9 +221,11 @@ function attachVideoSource(video, src) {
|
||||
hls.loadSource(src);
|
||||
hls.attachMedia(video);
|
||||
hlsInstances.set(video, hls);
|
||||
logEvent("preview:attach:hls", { src });
|
||||
return;
|
||||
}
|
||||
video.src = src;
|
||||
logEvent("preview:attach:file", { src });
|
||||
}
|
||||
|
||||
function detachVideoSource(video) {
|
||||
@@ -161,6 +233,7 @@ function detachVideoSource(video) {
|
||||
if (existing) {
|
||||
existing.destroy();
|
||||
hlsInstances.delete(video);
|
||||
logEvent("preview:detach:hls", { ok: true });
|
||||
}
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
@@ -188,11 +261,13 @@ function renderResults(results) {
|
||||
previewVideo.poster = item.thumbnailUrl || "";
|
||||
const mediaArea = node.querySelector(".relative");
|
||||
mediaArea.addEventListener("mouseenter", () => {
|
||||
logEvent("preview:hover:start", { title: item.title, source: item.source, previewVideoUrl: item.previewVideoUrl });
|
||||
overlays.forEach((overlay) => overlay.classList.add("hidden"));
|
||||
previewVideo.classList.remove("hidden");
|
||||
previewVideo.play().catch(() => {});
|
||||
});
|
||||
mediaArea.addEventListener("mouseleave", () => {
|
||||
logEvent("preview:hover:end", { title: item.title, source: item.source });
|
||||
previewVideo.pause();
|
||||
previewVideo.currentTime = 0;
|
||||
previewVideo.classList.add("hidden");
|
||||
@@ -219,6 +294,7 @@ searchForm.addEventListener("submit", async (event) => {
|
||||
searchWarning.textContent = data.warning;
|
||||
searchWarning.classList.remove("hidden");
|
||||
}
|
||||
logEvent("search:completed", { results: data.results?.length || 0, queries: data.queries || [] });
|
||||
setStatus("search complete", 100);
|
||||
} catch (error) {
|
||||
searchWarning.textContent = error.message;
|
||||
@@ -234,12 +310,14 @@ async function uploadFile(file) {
|
||||
uploadResult.textContent = "uploading...";
|
||||
try {
|
||||
await api("/api/upload", { method: "POST", body: formData });
|
||||
logEvent("upload:completed", { fileName: file.name, size: file.size });
|
||||
} catch (error) {
|
||||
uploadResult.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreviewModal(preview) {
|
||||
logEvent("preview:modal:open", preview);
|
||||
previewTitle.textContent = preview.title;
|
||||
previewThumbnail.src = preview.thumbnail;
|
||||
previewThumbnail.alt = preview.title;
|
||||
@@ -271,6 +349,7 @@ function openPreviewModal(preview) {
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
logEvent("preview:modal:close", { title: previewTitle.textContent });
|
||||
previewVideo.pause();
|
||||
detachVideoSource(previewVideo);
|
||||
previewMediaFrame.style.aspectRatio = "";
|
||||
@@ -332,6 +411,7 @@ downloadForm.addEventListener("submit", async (event) => {
|
||||
downloadResult.textContent = "preview loaded";
|
||||
} catch (error) {
|
||||
downloadResult.textContent = error.message;
|
||||
logEvent("download:preview:error", { message: error.message, data: error.data || null });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -355,6 +435,7 @@ confirmDownload.addEventListener("click", async () => {
|
||||
downloadResult.textContent = data.message;
|
||||
} catch (error) {
|
||||
downloadResult.textContent = error.message;
|
||||
logEvent("download:start:error", { message: error.message, data: error.data || null });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -399,11 +480,21 @@ for (const [thumb, name] of [[startThumb, "start"], [endThumb, "end"]]) {
|
||||
});
|
||||
}
|
||||
previewVideo.addEventListener("loadedmetadata", () => {
|
||||
logEvent("preview:modal:loadedmetadata", {
|
||||
width: previewVideo.videoWidth,
|
||||
height: previewVideo.videoHeight,
|
||||
src: previewVideo.currentSrc || previewVideo.src,
|
||||
});
|
||||
if (previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) {
|
||||
previewMediaFrame.style.aspectRatio = `${previewVideo.videoWidth} / ${previewVideo.videoHeight}`;
|
||||
}
|
||||
});
|
||||
previewThumbnail.addEventListener("load", () => {
|
||||
logEvent("preview:thumbnail:loaded", {
|
||||
width: previewThumbnail.naturalWidth,
|
||||
height: previewThumbnail.naturalHeight,
|
||||
src: previewThumbnail.currentSrc || previewThumbnail.src,
|
||||
});
|
||||
if (!previewVideo.src && previewThumbnail.naturalWidth > 0 && previewThumbnail.naturalHeight > 0) {
|
||||
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
||||
}
|
||||
@@ -421,6 +512,37 @@ for (const button of platformToggles) {
|
||||
});
|
||||
}
|
||||
|
||||
debugToggle.addEventListener("click", () => {
|
||||
debugPanel.classList.remove("hidden");
|
||||
logEvent("debug:panel:open", {});
|
||||
});
|
||||
closeDebugPanel.addEventListener("click", () => {
|
||||
debugPanel.classList.add("hidden");
|
||||
});
|
||||
clearLogs.addEventListener("click", () => {
|
||||
debugEntries.length = 0;
|
||||
renderLogs();
|
||||
});
|
||||
downloadLogs.addEventListener("click", () => {
|
||||
const blob = new Blob(
|
||||
[debugEntries.map((entry) => `[${entry.ts}] ${entry.type}\n${safeStringify(entry.payload)}\n`).join("\n")],
|
||||
{ type: "text/plain;charset=utf-8" },
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `ai-media-hub-${new Date().toISOString().replace(/[:.]/g, "-")}.log`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
window.addEventListener("error", (event) => {
|
||||
logEvent("window:error", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno });
|
||||
});
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
logEvent("window:unhandledrejection", { reason: String(event.reason) });
|
||||
});
|
||||
|
||||
connectWS();
|
||||
syncPlatformButtons();
|
||||
setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0);
|
||||
logEvent("app:ready", { activePlatforms: Array.from(activePlatforms) });
|
||||
|
||||
+20
-1
@@ -74,6 +74,25 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<button id="debugToggle" class="fixed bottom-4 right-4 z-40 rounded-full border border-white/15 bg-black/70 px-4 py-2 text-xs uppercase tracking-[0.25em] text-zinc-300 backdrop-blur">
|
||||
Logs
|
||||
</button>
|
||||
|
||||
<aside id="debugPanel" class="fixed bottom-4 right-4 z-40 hidden h-[60vh] w-[min(92vw,720px)] overflow-hidden rounded-3xl border border-white/10 bg-zinc-950/95 shadow-2xl backdrop-blur">
|
||||
<div class="flex items-center justify-between border-b border-white/10 px-4 py-3">
|
||||
<div>
|
||||
<p class="text-[11px] uppercase tracking-[0.25em] text-zinc-500">Developer Logs</p>
|
||||
<p id="debugSummary" class="text-sm text-zinc-300">No events yet</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="downloadLogs" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Download .log</button>
|
||||
<button id="clearLogs" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Clear</button>
|
||||
<button id="closeDebugPanel" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="debugLogList" class="h-[calc(60vh-78px)] overflow-auto px-3 py-3 font-mono text-xs text-zinc-200"></div>
|
||||
</aside>
|
||||
|
||||
<div id="previewModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 px-4">
|
||||
<div class="w-full max-w-5xl rounded-3xl border border-white/10 bg-zinc-950 p-5 shadow-2xl">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
@@ -144,6 +163,6 @@
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script src="/app.js?v=20260313h" defer></script>
|
||||
<script src="/app.js?v=20260313i" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,3 +31,35 @@ body {
|
||||
cursor: ew-resize;
|
||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
#debugLogList::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
#debugLogList::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.debug-entry {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.debug-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-entry__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.debug-entry__payload {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user