Update api

This commit is contained in:
sn0w
2026-10-29 16:16:26 +01:00
parent 0b48edc088
commit 80ca8b5419
5 changed files with 228 additions and 74 deletions
+68 -10
View File
@@ -1,6 +1,8 @@
import json
import os
import re
from pathlib import Path
from urllib.parse import quote
import folder_paths
from server import PromptServer
from aiohttp import web
@@ -138,6 +140,8 @@ async def fetch_wiki_page(request):
"""
Proxy endpoint to fetch Danbooru wiki pages.
This avoids CSP violations by making the request server-side.
Uses the official JSON API instead of scraping HTML, which is
blocked by Cloudflare bot protection (HTTP 403) for Python clients.
"""
tag = request.match_info["tag"]
if not tag:
@@ -146,34 +150,88 @@ async def fetch_wiki_page(request):
# Clean the tag similar to frontend logic
tag = tag.strip()
url = f"https://danbooru.donmai.us/wiki_pages/{tag}"
api_url = "https://danbooru.donmai.us/wiki_pages.json"
headers = {
# Danbooru requires API consumers to identify themselves.
"User-Agent": "ComfyUI-Syntax-Highlighting/1.0",
"Accept": "application/json",
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get(api_url, params={"search[title]": tag},
timeout=aiohttp.ClientTimeout(total=10)) as response:
if response.status != 200:
return web.json_response({
"success": False,
"error": {
"status": response.status,
"message": f"HTTP error! status: {response.status}",
"url": url,
"url": f"{api_url}?search[title]={quote(tag)}",
}
}, status=response.status)
html_content = await response.text()
})
pages = await response.json()
page = _find_wiki_page(pages, tag)
if page is None:
return web.json_response({
"success": False,
"error": {
"status": 404,
"message": "Wiki page not found",
"url": f"{api_url}?search[title]={quote(tag)}",
}
})
body = page.get("body") or ""
return web.json_response({
"success": True,
"data": html_content
"data": {
"title": page.get("title"),
"body": body,
"description": _extract_wiki_description(body),
}
})
except aiohttp.ClientError as e:
return web.json_response({ "success": False, "error": { "message": str(e), "url": url, }
return web.json_response({ "success": False, "error": { "message": str(e), "url": api_url, }
}, status=500)
except Exception as e:
return web.json_response({
"success": False,
"error": {
"message": f"Unexpected error: {str(e)}",
"url": url,
"url": api_url,
}
}, status=500)
def _find_wiki_page(pages, tag):
"""Prefer the exact title match, fall back to the first result."""
if not pages:
return None
lowered = tag.lower()
for page in pages:
if isinstance(page, dict) and str(page.get("title", "")).lower() == lowered:
return page
return pages[0] if isinstance(pages[0], dict) else None
def _extract_wiki_description(body):
"""Extract the first paragraph of a Danbooru DText wiki body."""
if not body:
return None
text = body.replace("\r\n", "\n").strip()
if not text:
return None
paragraphs = [p.strip() for p in text.split("\n\n")]
first = next((p for p in paragraphs if p), "")
if not first:
return None
# Unwrap DText wiki links: [[target|label]] -> label, [[target]] -> target
first = re.sub(r"\[\[([^|\]]+)\|([^\]]+)\]\]", r"\2", first)
first = re.sub(r"\[\[([^\]]+)\]\]", r"\1", first)
lines = []
for line in first.split("\n"):
line = re.sub(r"^(h[1-6]\.\s*|[*#\s]+)", "", line).strip()
if line:
lines.append(line)
description = " ".join(lines).strip()
return description or None
+79 -35
View File
@@ -113,6 +113,71 @@ export class BooruApi {
return element;
}
extractDescriptionFromDText(body) {
if (!body) return null;
const text = body.replaceAll("\r\n", "\n").trim();
if (!text) return null;
const paragraphs = text.split("\n\n").map((p) => p.trim());
const first = paragraphs.find((p) => p) ?? "";
if (!first) return null;
// Unwrap DText wiki links: [[target|label]] -> label, [[target]] -> target
const unwrapped = first
.replace(/\[\[([^|\]]+)\|([^\]]+)\]\]/g, "$2")
.replace(/\[\[([^\]]+)\]\]/g, "$1");
const lines = unwrapped
.split("\n")
.map((line) => line.replace(/^(h[1-6]\.\s*|[*#\s]+)/, "").trim())
.filter((line) => line);
return lines.join(" ").trim() || null;
}
extractDescriptionFromHtml(html) {
const body = this.getElementById(html, "wiki-page-body");
return body?.querySelector("p")?.textContent || null;
}
async fetchDescriptionDirect(cleanedTag) {
// Direct browser request: the browser TLS fingerprint passes
// Cloudflare protection that blocks server-side Python clients.
const url = `${this.baseUrl}/wiki_pages.json?search[title]=${encodeURIComponent(cleanedTag)}`;
const response = await this.#fetch(url);
if (!response.success || !Array.isArray(response.data) || response.data.length === 0) {
return null;
}
const page =
response.data.find((p) => p?.title?.toLowerCase() === cleanedTag.toLowerCase()) ??
response.data[0];
return this.extractDescriptionFromDText(page?.body);
}
async fetchDescriptionViaProxy(cleanedTag) {
const url = `${this.proxyUrl}/${encodeURIComponent(cleanedTag)}`;
const response = await this.#fetch(url, {
responseType: "html",
useProxy: true,
});
if (!response.success) {
return null;
}
const data = response.data;
if (typeof data === "string") {
// Legacy proxy response returning raw HTML.
return this.extractDescriptionFromHtml(data);
}
if (data && typeof data === "object") {
return data.description ?? this.extractDescriptionFromDText(data.body);
}
return null;
}
async resolveDescription(cleanedTag) {
// Prefer the direct request, fall back to the server proxy
// (e.g. when CSP blocks cross-origin requests from the browser).
const direct = await this.fetchDescriptionDirect(cleanedTag);
if (direct !== null) return direct;
return this.fetchDescriptionViaProxy(cleanedTag);
}
async getTagDescription(tag) {
const cleanedTag = this.cleanTag(tag);
@@ -130,31 +195,19 @@ export class BooruApi {
// Revalidate in background
(async () => {
try {
const url = `${this.proxyUrl}/${cleanedTag}`;
const response = await this.#fetch(url, {
responseType: "html",
useProxy: true,
});
if (response.success) {
const body = this.getElementById(
response.data,
"wiki-page-body"
);
const newDescription =
body?.querySelector("p")?.textContent || null;
// Update cache with fresh data
this.cache[cleanedTag] = {
description: newDescription,
timestamp: Date.now(),
};
this.saveCache();
// Trim cache if it exceeds max size
if (
Object.keys(this.cache).length >
this.MAX_CACHE_SIZE
) {
this.trimCache();
}
const newDescription = await this.resolveDescription(cleanedTag);
// Update cache with fresh data
this.cache[cleanedTag] = {
description: newDescription,
timestamp: Date.now(),
};
this.saveCache();
// Trim cache if it exceeds max size
if (
Object.keys(this.cache).length >
this.MAX_CACHE_SIZE
) {
this.trimCache();
}
} catch (e) {
console.warn("Background revalidation failed", e);
@@ -165,16 +218,7 @@ export class BooruApi {
}
// No cache: fetch fresh
const url = `${this.proxyUrl}/${cleanedTag}`;
const response = await this.#fetch(url, {
responseType: "html",
useProxy: true,
});
if (!response.success) {
return null;
}
const body = this.getElementById(response.data, "wiki-page-body");
const description = body?.querySelector("p")?.textContent || null;
const description = await this.resolveDescription(cleanedTag);
// Cache the result
this.cache[cleanedTag] = { description, timestamp: Date.now() };
+36 -28
View File
@@ -199,34 +199,42 @@ async function observeContextMenu(existingList) {
const favoriteColor = settings["Combo Highlight Color"];
const brighterFavoriteColor = brightenColor(favoriteColor);
const style = document.createElement("style");
style.textContent = `
.preview-image {
position: absolute;
z-index: 2000;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.preview-image.fade-in {
opacity: 1;
}
.preview-image.fade-out {
opacity: 0;
}
.litegraph.litecontextmenu.dark .litemenu-entry.litemenu-entry-custom {
background-color: ${favoriteColor} !important;
display: flex;
align-items: center;
}
.litemenu-entry:hover:not(.disabled):not(.separator).litemenu-entry-custom:hover {
background-color: ${brighterFavoriteColor} !important;
}
`;
document.head.appendChild(style);
const style = document.getElementById("comfy-ui-styles") || (() => {
const s = document.createElement("style");
s.id = "comfy-ui-styles";
document.head.appendChild(s);
return s;
})();
if (!style.dataset.contextMenuStylesAdded) {
style.textContent += `
.preview-image {
position: absolute;
z-index: 2000;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.preview-image.fade-in {
opacity: 1;
}
.preview-image.fade-out {
opacity: 0;
}
.litegraph.litecontextmenu.dark .litemenu-entry.litemenu-entry-custom {
background-color: ${favoriteColor} !important;
display: flex;
align-items: center;
}
.litemenu-entry:hover:not(.disabled):not(.separator).litemenu-entry-custom:hover {
background-color: ${brighterFavoriteColor} !important;
}
`;
style.dataset.contextMenuStylesAdded = "true";
}
let images;
try {
+4
View File
@@ -4,6 +4,10 @@ import { setOverlayStyle } from "./textbox/styleUtils.js";
import { api } from "../../../scripts/api.js";
import { settingsHelper } from "./settings.js";
const sharedStyle = document.createElement("style");
sharedStyle.id = "syntax-highlighting-custom-ui-styles";
document.head.appendChild(sharedStyle);
config.init();
api.addEventListener("update_text_highlight", async () => {
+41 -1
View File
@@ -4,6 +4,9 @@ import { settingsHelper } from "../settings.js";
const booruApi = new BooruApi();
let currentTooltipRequest = null;
const TAG_WIKI_BASE = "https://danbooru.donmai.us/wiki_pages/";
const resolvedTags = new Set();
export async function showTagTooltip(element, tag) {
const requestId = Symbol("tooltip-request");
currentTooltipRequest = requestId;
@@ -43,7 +46,12 @@ export async function showTagTooltip(element, tag) {
}
}, 100);
const description = (await booruApi.getTagDescription(tag)) ?? "No description available.";
const rawDescription = await booruApi.getTagDescription(tag);
const description = rawDescription ?? "No description available.";
if (rawDescription !== null) {
resolvedTags.add(booruApi.cleanTag(tag));
}
if (currentTooltipRequest !== requestId) {
clearTimeout(loadingTimeout);
@@ -108,6 +116,21 @@ export async function addTooltips(textarea) {
});
textarea.addEventListener("mousemove", (e) => {
if (e.ctrlKey || e.metaKey) {
textarea.style.pointerEvents = "none";
const el = document.elementFromPoint(e.clientX, e.clientY);
textarea.style.pointerEvents = "auto";
const tagSpan = el?.closest(".tag-span");
if (tagSpan) {
const tag = booruApi.cleanTag(tagSpan.dataset.tag);
textarea.style.cursor = resolvedTags.has(tag) ? "pointer" : "";
} else {
textarea.style.cursor = "";
}
} else {
textarea.style.cursor = "";
}
if (isTyping) return;
if (tooltipTimeout) {
@@ -135,10 +158,27 @@ export async function addTooltips(textarea) {
});
textarea.addEventListener("mouseout", () => {
textarea.style.cursor = "";
if (tooltipTimeout) {
clearTimeout(tooltipTimeout);
}
hideTagTooltip();
currentTooltipTag = null;
});
textarea.addEventListener("click", (e) => {
if (!e.ctrlKey && !e.metaKey) return;
textarea.style.pointerEvents = "none";
const elementBelow = document.elementFromPoint(e.clientX, e.clientY);
textarea.style.pointerEvents = "auto";
const tagSpan = elementBelow?.closest(".tag-span");
if (tagSpan) {
const tag = booruApi.cleanTag(tagSpan.dataset.tag);
if (!resolvedTags.has(tag)) return;
const newWindow = window.open(TAG_WIKI_BASE + encodeURIComponent(tag), "_blank");
if (newWindow) newWindow.focus();
}
});
}