Update api
This commit is contained in:
+68
-10
@@ -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
@@ -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() };
|
||||
|
||||
@@ -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,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 () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user