Release 0.2.0: CivitAI domain compatibility
This commit is contained in:
@@ -1,22 +1,56 @@
|
||||
# ComfyUI CivitAI ImagePlus
|
||||
|
||||
**Browse CivitAI images in ComfyUI, preview protected images, and edit prompts before you generate and pass to image 2 image.**
|
||||
Compatible with ComfyUI 0.6+
|
||||
**Browse CivitAI images in ComfyUI, preview protected images, edit prompts before generating, and pass selected images into img2img workflows.**
|
||||
|
||||
ComfyUI_CivitAI_ImagePlus adds a CivitAI gallery browser to ComfyUI, plus helper nodes for **image preview with I2I connector**, **prompt editing**, and **quick info**.
|
||||
Nodes are populated instantly without a need to run the workflow.
|
||||
**Tested on current stable ComfyUI releases (verified on 0.19.x at time of writing).**
|
||||
|
||||
ComfyUI_CivitAI_ImagePlus adds a CivitAI gallery browser to ComfyUI, plus helper nodes for:
|
||||
|
||||
- **image preview with img2img connector**
|
||||
- **prompt editing**
|
||||
- **quick info display**
|
||||
|
||||
Nodes are populated instantly without needing to run the workflow first.
|
||||
|
||||
---
|
||||
|
||||
## ✅ What you can do
|
||||
|
||||
- **Open a full-screen CivitAI gallery, display Most Reactions, Most Comments and Newest** from inside ComfyUI
|
||||
- **Open a full-screen CivitAI gallery** from inside ComfyUI
|
||||
- Browse images using:
|
||||
- **Most Reactions**
|
||||
- **Most Comments**
|
||||
- **Newest**
|
||||
- **Pick an image** and instantly:
|
||||
- preview it (including **protected** images if you have an API key)
|
||||
- preview it
|
||||
- preview **protected** images if you have a valid CivitAI API key
|
||||
- load its prompts into an editor
|
||||
- see a handy info panel
|
||||
- **Fetch by URL** (paste a CivitAI link)
|
||||
- Use the preview image for **img2img**
|
||||
- **Fetch by URL** using a CivitAI page link
|
||||
- Use the selected preview image as input for **img2img** workflows
|
||||
|
||||
---
|
||||
|
||||
## 🌐 CivitAI domain compatibility
|
||||
|
||||
CivitAI now uses separate public-facing domains:
|
||||
|
||||
- `https://civitai.com`
|
||||
- `https://civitai.red`
|
||||
|
||||
This plugin supports both for:
|
||||
|
||||
- **Fetch URL** input
|
||||
- copied page links
|
||||
- protected preview routing
|
||||
|
||||
The gallery includes a **Page Links** selector so you can choose:
|
||||
|
||||
- **Auto**
|
||||
- **Civitai.com**
|
||||
- **Civitai.red**
|
||||
|
||||
If you paste a CivitAI URL directly, the plugin will try to preserve that domain automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -28,75 +62,49 @@ Example workflows:
|
||||
- [workflows/Gallery_image_to_image.json](https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus/blob/main/workflows/Gallery_image_to_image.json)
|
||||
- [workflows/Gallery_Z-image_Turbo.json](https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus/blob/main/workflows/Gallery_Z-image_Turbo.json)
|
||||
|
||||
**Note:**
|
||||
Gallery text to image and Gallery image to image workflows use ComfyUI core nodes plus this node pack.
|
||||
Gallery Z-image Turbo workflow uses additional custome nodes.
|
||||
Click the Fetch button to grab a random test image, the test image is available without an API key.
|
||||
**Note:**
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots
|
||||
Image2Image and Text2Image workflow examples.
|
||||
Alternative 'Show Text' or 'Show Any' nodes can be used but they will not populate automatically, Run will need to initiated first.
|
||||
They must also be compatible with Node2 requirements.
|
||||
|
||||
**Nodes**
|
||||
To search for the nodes type Civit
|
||||
|
||||
<p align="center">
|
||||
<a href="examples/search.jpg">
|
||||
<img src="examples/search.jpg" width="300" alt="Image 2 Image example" />
|
||||
</a>
|
||||
<a href="examples/nodes.jpg">
|
||||
<img src="examples/nodes.jpg" width="300" alt="Nodes" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Workflow examples**
|
||||
|
||||
<p align="center">
|
||||
<a href="examples/gallery_T2I.jpg">
|
||||
<img src="examples/gallery_T2I.jpg" width="300" alt="Text 2 Image example" />
|
||||
</a>
|
||||
<a href="examples/gallery_I2I.jpg">
|
||||
<img src="examples/gallery_I2I.jpg" width="300" alt="Image 2 Image example" />
|
||||
</a>
|
||||
<a href="examples/Alt_nodes.jpg">
|
||||
<img src="examples/Alt_nodes.jpg" width="300" alt="Alternate nodes can be used (show text, show any), they will not populate until Run is initiated." />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center"><em>Click any image to open full size.</em></p>
|
||||
- The text-to-image and image-to-image workflows use ComfyUI core nodes plus this node pack.
|
||||
- The Z-image Turbo workflow uses additional custom nodes.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Installation
|
||||
|
||||
### Option A — ComfyUI Manager
|
||||
### Option A — ComfyUI Manager / Registry
|
||||
|
||||
1. Open **ComfyUI → Manager**
|
||||
2. Install **ComfyUI-CivitAI-ImagePlus**
|
||||
3. Restart ComfyUI
|
||||
2. Search for **CivitAI ImagePlus**
|
||||
3. Install the node pack
|
||||
4. Restart ComfyUI
|
||||
|
||||
### Option B — Manual
|
||||
|
||||
1. Go to your ComfyUI `custom_nodes` folder:
|
||||
- `ComfyUI/custom_nodes/`
|
||||
2. Clone this repo:
|
||||
```bash
|
||||
git clone https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus.git
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus.git
|
||||
```
|
||||
|
||||
3. Restart ComfyUI
|
||||
|
||||
### Option C — (Zip)
|
||||
1. If your having trouble with Git, click the Code button, download as Zip and extract the folder to your ComfyUI custom_nodes folder.
|
||||
2. Restart ComfyUI
|
||||
### Option C — ZIP
|
||||
|
||||
1. If you are having trouble with Git, click the **Code** button on GitHub and download the repository as a ZIP
|
||||
2. Extract the folder into your ComfyUI `custom_nodes` directory
|
||||
3. Restart ComfyUI
|
||||
|
||||
---
|
||||
|
||||
## 🔑 CivitAI API Key (required for protected images)
|
||||
|
||||
To preview and fetch **protected** content, you need a CivitAI API key.
|
||||
|
||||
1. Create the file if it doesn't exist:
|
||||
1. Create the file if it does not exist:
|
||||
|
||||
```
|
||||
```txt
|
||||
custom_nodes/ComfyUI_CivitAI_ImagePlus/api_key.txt
|
||||
```
|
||||
|
||||
@@ -106,45 +114,63 @@ custom_nodes/ComfyUI_CivitAI_ImagePlus/api_key.txt
|
||||
CIVITAI_API_KEY=YOUR_CIVITAI_API_KEY_HERE
|
||||
```
|
||||
|
||||
✅ Restart ComfyUI after adding/changing the key.
|
||||
✅ Restart ComfyUI after adding or changing the key.
|
||||
|
||||
### Security note
|
||||
|
||||
Protected image previews are handled **server-side**.
|
||||
|
||||
- Your API key stays on your machine in `api_key.txt`
|
||||
- The browser UI does **not** need direct access to your key
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Nodes (what each one does)
|
||||
## 🧱 Nodes
|
||||
|
||||
### 1) **CivitAI Gallery**
|
||||
|
||||
Open the gallery and select images.
|
||||
|
||||
- **Open Gallery**: full-screen browsing
|
||||
- **Fetch URL** + **Fetch**: paste a CivitAI URL and load the image
|
||||
- **Open Gallery** → full-screen browsing
|
||||
- **Fetch URL** + **Fetch** → paste a CivitAI URL and load the image
|
||||
|
||||
**Tip:** you can use a **post URL** (example: `https://civitai.com/posts/<id>`) or an **image URL** (example: `https://civitai.com/images/<id>`)
|
||||
**Tip:** you can use either:
|
||||
|
||||
- a **post URL** like `https://civitai.com/posts/<id>` or `https://civitai.red/posts/<id>`
|
||||
- an **image URL** like `https://civitai.com/images/<id>` or `https://civitai.red/images/<id>`
|
||||
|
||||
---
|
||||
|
||||
### 2) **CivitAI Image Preview**
|
||||
|
||||
- **Copy CivitAI Page URL** button: copies the CivitAI page link for the selected image (the page that shows generation info)
|
||||
- Output: `IMAGE` (great for **img2img**)
|
||||
- Shows the selected preview image directly in the node
|
||||
- Includes **Copy CivitAI Page URL** button
|
||||
- Output: `IMAGE`
|
||||
|
||||
This makes it ideal for **img2img** workflows.
|
||||
|
||||
---
|
||||
|
||||
### 3) **CivitAI Prompt Editor**
|
||||
|
||||
Lets you edit prompts before generating.
|
||||
|
||||
- Shows **Positive** + **Negative** previews right on the node
|
||||
- Buttons at the bottom:
|
||||
- **Edit Positive** / **Edit Negative** (opens a big editor window)
|
||||
- **Restore** (revert to the original prompts from the last selection)
|
||||
- **Clear** (wipe prompts)
|
||||
- Shows **Positive** and **Negative** previews directly on the node
|
||||
- Bottom buttons:
|
||||
- **Edit Positive**
|
||||
- **Edit Negative**
|
||||
- **Restore** → revert to prompts from the last selection
|
||||
- **Clear** → wipe both prompts
|
||||
|
||||
Output:
|
||||
- Positive (STRING)
|
||||
- Negative (STRING)
|
||||
Outputs:
|
||||
|
||||
- `Positive` (`STRING`)
|
||||
- `Negative` (`STRING`)
|
||||
|
||||
---
|
||||
|
||||
### 4) **CivitAI Info Display**
|
||||
|
||||
A small info preview panel that updates instantly when you select an image.
|
||||
|
||||
---
|
||||
@@ -152,12 +178,14 @@ A small info preview panel that updates instantly when you select an image.
|
||||
## 🔁 Recommended workflow setups
|
||||
|
||||
### A) txt2img (browse → edit → generate)
|
||||
|
||||
1. Select an image in **CivitAI Gallery**
|
||||
2. Edit prompts in **CivitAI Prompt Editor**
|
||||
3. Connect Prompt Editor outputs to your text/CLIP encode nodes
|
||||
3. Connect Prompt Editor outputs to your text / CLIP encode nodes
|
||||
4. Generate
|
||||
|
||||
### B) img2img (preview → use as init image)
|
||||
|
||||
1. Select an image in **CivitAI Gallery**
|
||||
2. Confirm it in **CivitAI Image Preview**
|
||||
3. Use Preview output as your **img2img init image**
|
||||
@@ -168,24 +196,42 @@ A small info preview panel that updates instantly when you select an image.
|
||||
## 🛠 Troubleshooting
|
||||
|
||||
### Nothing updates instantly
|
||||
- Restart ComfyUI
|
||||
- Hard refresh your browser (Ctrl+F5)
|
||||
|
||||
### Protected images won’t preview
|
||||
- Restart ComfyUI
|
||||
- Hard refresh your browser (`Ctrl+F5`)
|
||||
|
||||
### Protected images will not preview
|
||||
|
||||
- Confirm `api_key.txt` exists and is formatted correctly:
|
||||
`CIVITAI_API_KEY=...`
|
||||
|
||||
```txt
|
||||
CIVITAI_API_KEY=...
|
||||
```
|
||||
|
||||
- Restart ComfyUI
|
||||
|
||||
### Fetch says “No items returned”
|
||||
- Prefer using a **post URL** (`/posts/<id>`)
|
||||
- Some content may still be restricted by your CivitAI browsing/account settings
|
||||
|
||||
- Prefer using a **post URL** (`/posts/<id>`) when possible
|
||||
- Some content may still be restricted by CivitAI account, domain, or region visibility rules
|
||||
|
||||
### Page links open on the wrong domain
|
||||
|
||||
Use the gallery’s **Page Links** selector:
|
||||
|
||||
- **Auto**
|
||||
- **Civitai.com**
|
||||
- **Civitai.red**
|
||||
|
||||
If you fetch by URL, the plugin will try to preserve that domain automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security notes
|
||||
## 🧪 Compatibility notes
|
||||
|
||||
- Your API key stays on your machine in `api_key.txt`.
|
||||
- The browser does **not** need your key.
|
||||
- Tested on current stable ComfyUI builds
|
||||
- Uses standard ComfyUI frontend extension registration
|
||||
- If you are testing against nightly/master builds, upstream frontend changes may land before a stable release
|
||||
|
||||
---
|
||||
|
||||
@@ -193,8 +239,8 @@ A small info preview panel that updates instantly when you select an image.
|
||||
|
||||
MIT License (see `LICENSE`).
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
Inspired by earlier community work exploring similar workflows and UI ideas.
|
||||
|
||||
|
||||
+367
-86
@@ -1,4 +1,3 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import aiohttp
|
||||
@@ -9,7 +8,8 @@ import numpy as np
|
||||
from PIL import Image
|
||||
import io
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse, quote
|
||||
from urllib.parse import urlparse, quote, parse_qs
|
||||
from typing import Optional
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# API key handling
|
||||
@@ -17,6 +17,19 @@ from urllib.parse import urlparse, quote
|
||||
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
API_KEY_FILE = os.path.join(NODE_DIR, "api_key.txt")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Config / future-proofing
|
||||
# ------------------------------------------------------------
|
||||
DEFAULT_SITE_API_BASES = (
|
||||
"https://civitai.com/api/v1",
|
||||
"https://civitai.red/api/v1",
|
||||
)
|
||||
|
||||
DEFAULT_PAGE_BASES = {
|
||||
"com": "https://civitai.com",
|
||||
"red": "https://civitai.red",
|
||||
}
|
||||
|
||||
|
||||
def load_api_key():
|
||||
if not os.path.exists(API_KEY_FILE):
|
||||
@@ -29,11 +42,37 @@ def load_api_key():
|
||||
return None
|
||||
|
||||
|
||||
def _get_site_api_bases():
|
||||
"""
|
||||
Future-proof:
|
||||
- allows override via env var
|
||||
- tries both .com and .red
|
||||
"""
|
||||
env_base = (os.getenv("CIVITAI_SITE_API_BASE") or "").strip().rstrip("/")
|
||||
bases = []
|
||||
if env_base:
|
||||
bases.append(env_base)
|
||||
for b in DEFAULT_SITE_API_BASES:
|
||||
if b not in bases:
|
||||
bases.append(b)
|
||||
return tuple(bases)
|
||||
|
||||
|
||||
def _get_page_domain_mode():
|
||||
"""
|
||||
auto | com | red
|
||||
"""
|
||||
mode = (os.getenv("CIVITAI_PAGE_DOMAIN_MODE") or "auto").strip().lower()
|
||||
if mode not in ("auto", "com", "red"):
|
||||
return "auto"
|
||||
return mode
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Simple in-memory stores (keyed by node unique_id)
|
||||
# ------------------------------------------------------------
|
||||
PROMPT_STORE = {} # unique_id -> {"positive": str, "negative": str, "rev": int}
|
||||
PREVIEW_STORE = {} # unique_id -> {"url": str, "rev": int}
|
||||
PROMPT_STORE = {} # unique_id -> {"positive": str, "negative": str, "rev": int}
|
||||
PREVIEW_STORE = {} # unique_id -> {"url": str, "page_url": str, "rev": int}
|
||||
|
||||
|
||||
def _bump(store: dict, unique_id: str):
|
||||
@@ -60,9 +99,29 @@ def _redact_params(d: dict):
|
||||
return safe
|
||||
|
||||
|
||||
def _is_allowed_page_host(host: str) -> bool:
|
||||
host = (host or "").lower()
|
||||
return host in {
|
||||
"civitai.com",
|
||||
"www.civitai.com",
|
||||
"civitai.red",
|
||||
"www.civitai.red",
|
||||
}
|
||||
|
||||
|
||||
def _is_allowed_page_url(raw_url: str) -> bool:
|
||||
try:
|
||||
u = urlparse(raw_url)
|
||||
if u.scheme not in ("http", "https"):
|
||||
return False
|
||||
return _is_allowed_page_host(u.netloc)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _is_allowed_image_url(raw_url: str) -> bool:
|
||||
"""
|
||||
Prevent SSRF: only allow civitai image hosts.
|
||||
Prevent SSRF: only allow known civitai hosts.
|
||||
"""
|
||||
try:
|
||||
u = urlparse(raw_url)
|
||||
@@ -75,17 +134,86 @@ def _is_allowed_image_url(raw_url: str) -> bool:
|
||||
return True
|
||||
if host == "civitai.com":
|
||||
return True
|
||||
if host.endswith(".civitai.red"):
|
||||
return True
|
||||
if host == "civitai.red":
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def fetch_bytes_authed(url: str, api_key: str, timeout_s: int = 60) -> tuple[bytes, str]:
|
||||
headers = {
|
||||
def _page_base_from_url(raw_url: str) -> Optional[str]:
|
||||
if not raw_url:
|
||||
return None
|
||||
try:
|
||||
u = urlparse(raw_url)
|
||||
if u.scheme not in ("http", "https"):
|
||||
return None
|
||||
if not _is_allowed_page_host(u.netloc):
|
||||
return None
|
||||
return f"{u.scheme}://{u.netloc}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _preferred_page_base(
|
||||
explicit_url: str = "",
|
||||
source_url: str = "",
|
||||
page_domain: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
Resolution order:
|
||||
1) explicit URL (e.g. fetched post/image URL)
|
||||
2) page_domain forced by frontend ("com" or "red")
|
||||
3) source URL if it is a civitai page URL
|
||||
4) env override mode
|
||||
5) safe default: civitai.com
|
||||
"""
|
||||
forced = (page_domain or "").strip().lower()
|
||||
if forced in DEFAULT_PAGE_BASES:
|
||||
return DEFAULT_PAGE_BASES[forced]
|
||||
|
||||
base = _page_base_from_url(explicit_url)
|
||||
if base:
|
||||
return base
|
||||
|
||||
base = _page_base_from_url(source_url)
|
||||
if base:
|
||||
return base
|
||||
|
||||
mode = _get_page_domain_mode()
|
||||
if mode in DEFAULT_PAGE_BASES:
|
||||
return DEFAULT_PAGE_BASES[mode]
|
||||
|
||||
return DEFAULT_PAGE_BASES["com"]
|
||||
|
||||
|
||||
def _build_civitai_page_url(image_id=None, post_id=None, page_base: str = ""):
|
||||
base = (page_base or DEFAULT_PAGE_BASES["com"]).rstrip("/")
|
||||
if image_id:
|
||||
return f"{base}/images/{image_id}"
|
||||
if post_id:
|
||||
return f"{base}/posts/{post_id}"
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_headers(api_key: str, referer_url: str = "") -> dict:
|
||||
referer_base = _preferred_page_base(explicit_url=referer_url)
|
||||
return {
|
||||
"User-Agent": "ComfyUI-CivitAI-Gallery",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Referer": "https://civitai.com/",
|
||||
"Referer": referer_base.rstrip("/") + "/",
|
||||
}
|
||||
|
||||
|
||||
async def fetch_bytes_authed(
|
||||
url: str,
|
||||
api_key: str,
|
||||
timeout_s: int = 60,
|
||||
referer_url: str = "",
|
||||
) -> tuple[bytes, str]:
|
||||
headers = _auth_headers(api_key, referer_url=referer_url)
|
||||
timeout = aiohttp.ClientTimeout(total=timeout_s)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
@@ -97,6 +225,137 @@ async def fetch_bytes_authed(url: str, api_key: str, timeout_s: int = 60) -> tup
|
||||
return data, content_type
|
||||
|
||||
|
||||
def fetch_bytes_authed_sync(
|
||||
url: str,
|
||||
api_key: str,
|
||||
timeout_s: int = 60,
|
||||
referer_url: str = "",
|
||||
) -> tuple[bytes, str]:
|
||||
headers = _auth_headers(api_key, referer_url=referer_url)
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout_s) as r:
|
||||
data = r.read()
|
||||
content_type = r.headers.get("Content-Type", "application/octet-stream")
|
||||
return data, content_type
|
||||
|
||||
|
||||
async def _call_site_api_json(
|
||||
path: str,
|
||||
params: dict,
|
||||
api_key: str,
|
||||
timeout_s: int = 60,
|
||||
):
|
||||
"""
|
||||
Try configured API bases in order. This future-proofs against
|
||||
any host-level routing changes while keeping auth in headers.
|
||||
"""
|
||||
clean_params = dict(params or {})
|
||||
headers = _auth_headers(api_key)
|
||||
timeout = aiohttp.ClientTimeout(total=timeout_s)
|
||||
|
||||
last_error = None
|
||||
for base in _get_site_api_bases():
|
||||
url = base.rstrip("/") + "/" + path.lstrip("/")
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, params=clean_params, headers=headers) as resp:
|
||||
text = await resp.text()
|
||||
if resp.status != 200:
|
||||
last_error = {
|
||||
"error": "CivitAI request failed",
|
||||
"status": resp.status,
|
||||
"details": text[:800],
|
||||
"params": _redact_params(clean_params),
|
||||
"url": url,
|
||||
}
|
||||
continue
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except Exception:
|
||||
last_error = {
|
||||
"error": "CivitAI returned non-JSON response",
|
||||
"status": resp.status,
|
||||
"details": text[:800],
|
||||
"params": _redact_params(clean_params),
|
||||
"url": url,
|
||||
}
|
||||
continue
|
||||
return data, None
|
||||
except Exception as e:
|
||||
last_error = {
|
||||
"error": "CivitAI request exception",
|
||||
"details": str(e),
|
||||
"params": _redact_params(clean_params),
|
||||
"url": url,
|
||||
}
|
||||
|
||||
return None, last_error or {"error": "Unknown CivitAI API failure"}
|
||||
|
||||
|
||||
def _safe_int(value, default, min_value=None, max_value=None):
|
||||
try:
|
||||
n = int(value)
|
||||
except Exception:
|
||||
n = default
|
||||
if min_value is not None:
|
||||
n = max(min_value, n)
|
||||
if max_value is not None:
|
||||
n = min(max_value, n)
|
||||
return n
|
||||
|
||||
|
||||
def _iter_lookup_param_variants(id_key: str, id_value: str):
|
||||
"""
|
||||
Future-proof against possible API changes around content filtering.
|
||||
Try a few variants instead of assuming one forever.
|
||||
"""
|
||||
base = {id_key: id_value, "limit": 1}
|
||||
yield dict(base)
|
||||
yield dict(base, nsfw="X")
|
||||
yield dict(base, nsfw="true")
|
||||
yield dict(base, nsfw=True)
|
||||
|
||||
|
||||
def _maybe_fetch_tensor_from_image_url(image_url: str, page_url: str = "") -> torch.Tensor:
|
||||
"""
|
||||
Best-effort:
|
||||
- try authed fetch first when API key exists
|
||||
- otherwise try plain public fetch
|
||||
"""
|
||||
if not image_url:
|
||||
return torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
|
||||
api_key = load_api_key()
|
||||
|
||||
# First: try authenticated fetch when we can
|
||||
if api_key and _is_allowed_image_url(image_url):
|
||||
try:
|
||||
img_bytes, _ = fetch_bytes_authed_sync(
|
||||
image_url,
|
||||
api_key,
|
||||
timeout_s=30,
|
||||
referer_url=page_url,
|
||||
)
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
return _tensor_from_pil(img)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: direct public fetch
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
image_url,
|
||||
headers={"User-Agent": "ComfyUI-CivitAI-Gallery"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
img_bytes = r.read()
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
return _tensor_from_pil(img)
|
||||
except Exception as e:
|
||||
print(f"[CivitAI Gallery] Image download failed: {e}")
|
||||
return torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Nodes
|
||||
# ------------------------------------------------------------
|
||||
@@ -126,13 +385,6 @@ class CivitaiGalleryNode:
|
||||
)
|
||||
return positive or "", negative or ""
|
||||
|
||||
def _build_civitai_page_url(self, image_id=None, post_id=None):
|
||||
if image_id:
|
||||
return f"https://civitai.com/images/{image_id}"
|
||||
if post_id:
|
||||
return f"https://civitai.com/posts/{post_id}"
|
||||
return ""
|
||||
|
||||
def run(self, selection_data="{}"):
|
||||
try:
|
||||
data = json.loads(selection_data) if selection_data else {}
|
||||
@@ -157,20 +409,31 @@ class CivitaiGalleryNode:
|
||||
|
||||
positive, negative = self._extract_prompts(meta)
|
||||
|
||||
# Best-effort image tensor (protected-safe preview uses proxy route)
|
||||
tensor = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
image_url = item.get("url")
|
||||
if image_url:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
image_url, headers={"User-Agent": "ComfyUI-CivitAI-Gallery"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
img_bytes = r.read()
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
tensor = _tensor_from_pil(img)
|
||||
except Exception as e:
|
||||
print(f"[CivitAI Gallery] Image download failed: {e}")
|
||||
page_url = (
|
||||
item.get("page_url")
|
||||
or item.get("pageUrl")
|
||||
or ""
|
||||
)
|
||||
source_page_url = (
|
||||
item.get("source_page_url")
|
||||
or item.get("sourcePageUrl")
|
||||
or ""
|
||||
)
|
||||
page_domain = (
|
||||
item.get("page_domain")
|
||||
or item.get("pageDomain")
|
||||
or ""
|
||||
)
|
||||
|
||||
page_base = _preferred_page_base(
|
||||
explicit_url=page_url,
|
||||
source_url=source_page_url,
|
||||
page_domain=page_domain,
|
||||
)
|
||||
if not page_url:
|
||||
page_url = _build_civitai_page_url(image_id, post_id, page_base=page_base)
|
||||
|
||||
tensor = _maybe_fetch_tensor_from_image_url(item.get("url"), page_url=page_url)
|
||||
|
||||
model_name = ""
|
||||
try:
|
||||
@@ -180,7 +443,6 @@ class CivitaiGalleryNode:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
page_url = self._build_civitai_page_url(image_id, post_id)
|
||||
info = f"CivitAI Page: {page_url}" + (f"\nModel: {model_name}" if model_name else "")
|
||||
if not positive and not negative:
|
||||
info = "No prompts found.\n" + info
|
||||
@@ -224,7 +486,6 @@ class CivitaiInfoDisplayNode:
|
||||
Display-only node:
|
||||
- No inputs
|
||||
- No outputs
|
||||
- JS adds a textarea widget and updates it instantly.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -275,19 +536,17 @@ class CivitaiImagePreviewNode:
|
||||
return (image,)
|
||||
|
||||
uid = str(unique_id) if unique_id is not None else ""
|
||||
url = (PREVIEW_STORE.get(uid) or {}).get("url", "") or ""
|
||||
entry = PREVIEW_STORE.get(uid) or {}
|
||||
url = (entry.get("url", "") or "").strip()
|
||||
page_url = (entry.get("page_url", "") or "").strip()
|
||||
|
||||
if not url:
|
||||
tensor = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
return (tensor,)
|
||||
|
||||
# Fetch via local proxy route to keep auth handling consistent
|
||||
try:
|
||||
local = f"http://127.0.0.1:8188/civitai_gallery/proxy_image?url={quote(url, safe='')}"
|
||||
req = urllib.request.Request(local, headers={"User-Agent": "ComfyUI-CivitAI-Gallery"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
img_bytes = r.read()
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
return (_tensor_from_pil(img),)
|
||||
tensor = _maybe_fetch_tensor_from_image_url(url, page_url=page_url)
|
||||
return (tensor,)
|
||||
except Exception as e:
|
||||
print(f"[CivitAI Preview] Failed to fetch image: {e}")
|
||||
tensor = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
@@ -329,7 +588,7 @@ async def civitai_set_prompt(request):
|
||||
@prompt_server.routes.post("/civitai_gallery/set_preview")
|
||||
async def civitai_set_preview(request):
|
||||
"""
|
||||
Body: {"node_id": "<id>", "url": "..."}
|
||||
Body: {"node_id": "<id>", "url": "...", "page_url": "..."}
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
@@ -341,8 +600,11 @@ async def civitai_set_preview(request):
|
||||
return web.json_response({"error": "Missing node_id"}, status=400)
|
||||
|
||||
url = (body.get("url", "") or "").strip()
|
||||
page_url = (body.get("page_url", "") or body.get("pageUrl", "") or "").strip()
|
||||
|
||||
entry = PREVIEW_STORE.get(node_id) or {"rev": 0}
|
||||
entry["url"] = url
|
||||
entry["page_url"] = page_url
|
||||
PREVIEW_STORE[node_id] = entry
|
||||
_bump(PREVIEW_STORE, node_id)
|
||||
|
||||
@@ -356,31 +618,39 @@ async def civitai_images(request):
|
||||
return web.json_response({"error": "CivitAI API key missing (api_key.txt)"}, status=401)
|
||||
|
||||
p = dict(request.query)
|
||||
limit = int(p.get("limit", "36"))
|
||||
limit = _safe_int(p.get("limit", "36"), 36, min_value=1, max_value=200)
|
||||
sort = p.get("sort", "Most Reactions")
|
||||
period = p.get("period", "AllTime")
|
||||
nsfw = p.get("nsfw", "None")
|
||||
nsfw = (p.get("nsfw", "None") or "").strip()
|
||||
cursor = p.get("cursor", None)
|
||||
if cursor is not None:
|
||||
cursor = str(cursor).strip()
|
||||
if cursor == "":
|
||||
cursor = None
|
||||
|
||||
api_url = "https://civitai.com/api/v1/images"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
params = {"limit": min(200, limit), "sort": sort, "period": period, "nsfw": nsfw}
|
||||
params = {
|
||||
"limit": limit,
|
||||
"sort": sort,
|
||||
"period": period,
|
||||
}
|
||||
if nsfw:
|
||||
params["nsfw"] = nsfw
|
||||
if cursor is not None:
|
||||
params["cursor"] = cursor
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(api_url, params=params, headers=headers) as resp:
|
||||
data = await resp.json()
|
||||
return web.json_response(
|
||||
{"items": data.get("items", []), "metadata": data.get("metadata", {}) or {}}
|
||||
)
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
data, err = await _call_site_api_json("/images", params, api_key)
|
||||
if err and "nsfw" in params:
|
||||
# Fallback in case the API changes NSFW filter behavior
|
||||
params2 = dict(params)
|
||||
params2.pop("nsfw", None)
|
||||
data, err = await _call_site_api_json("/images", params2, api_key)
|
||||
|
||||
if err:
|
||||
return web.json_response(err, status=err.get("status", 500))
|
||||
|
||||
return web.json_response(
|
||||
{"items": data.get("items", []), "metadata": data.get("metadata", {}) or {}}
|
||||
)
|
||||
|
||||
|
||||
@prompt_server.routes.get("/civitai_gallery/image_by_url")
|
||||
@@ -393,15 +663,13 @@ async def civitai_image_by_url(request):
|
||||
if not raw_url:
|
||||
return web.json_response({"error": "Missing url parameter"}, status=400)
|
||||
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
if not _is_allowed_page_url(raw_url):
|
||||
return web.json_response({"error": "Only civitai.com and civitai.red page URLs are supported"}, status=400)
|
||||
|
||||
parsed = urlparse(raw_url)
|
||||
qs = parse_qs(parsed.query)
|
||||
path_parts = [p for p in parsed.path.strip("/").split("/") if p]
|
||||
|
||||
images_api = "https://civitai.com/api/v1/images"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
image_id = None
|
||||
post_id = None
|
||||
|
||||
@@ -421,52 +689,57 @@ async def civitai_image_by_url(request):
|
||||
if not post_id:
|
||||
post_id = (qs.get("postId") or [None])[0]
|
||||
|
||||
async def _call(params):
|
||||
params = dict(params)
|
||||
params["token"] = api_key
|
||||
params.setdefault("nsfw", "X") # Option A
|
||||
page_base = _preferred_page_base(explicit_url=raw_url)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(images_api, params=params, headers=headers) as resp:
|
||||
if resp.status != 200:
|
||||
text = await resp.text()
|
||||
return None, {
|
||||
"error": "CivitAI request failed",
|
||||
"status": resp.status,
|
||||
"details": text,
|
||||
"params": _redact_params(params),
|
||||
}
|
||||
data = await resp.json()
|
||||
return data, None
|
||||
async def _lookup(id_key: str, id_value: str):
|
||||
last_err = None
|
||||
for params in _iter_lookup_param_variants(id_key, id_value):
|
||||
data, err = await _call_site_api_json("/images", params, api_key)
|
||||
if err:
|
||||
last_err = err
|
||||
continue
|
||||
items = data.get("items", []) if isinstance(data, dict) else []
|
||||
if items:
|
||||
item = dict(items[0])
|
||||
item["pageUrl"] = _build_civitai_page_url(
|
||||
image_id=item.get("id") if id_key == "imageId" else None,
|
||||
post_id=id_value if id_key == "postId" else item.get("postId"),
|
||||
page_base=page_base,
|
||||
)
|
||||
item["sourcePageUrl"] = raw_url
|
||||
if page_base.endswith(".red"):
|
||||
item["pageDomain"] = "red"
|
||||
elif page_base.endswith(".com"):
|
||||
item["pageDomain"] = "com"
|
||||
return item, None
|
||||
return None, last_err
|
||||
|
||||
if post_id:
|
||||
data, err = await _call({"postId": post_id, "limit": 1})
|
||||
item, err = await _lookup("postId", post_id)
|
||||
if item:
|
||||
return web.json_response({"item": item})
|
||||
if err:
|
||||
return web.json_response(err, status=err.get("status", 500))
|
||||
items = data.get("items", []) if isinstance(data, dict) else []
|
||||
if items:
|
||||
return web.json_response({"item": items[0]})
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "No items returned for postId",
|
||||
"postId": post_id,
|
||||
"hint": "If this is a video-only post, /api/v1/images may return empty. Otherwise it may be restricted by browsing settings.",
|
||||
"hint": "The post may not currently expose an image through the images API, or visibility may be limited by account/domain/region settings.",
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if image_id:
|
||||
data, err = await _call({"imageId": image_id, "limit": 1})
|
||||
item, err = await _lookup("imageId", image_id)
|
||||
if item:
|
||||
return web.json_response({"item": item})
|
||||
if err:
|
||||
return web.json_response(err, status=err.get("status", 500))
|
||||
items = data.get("items", []) if isinstance(data, dict) else []
|
||||
if items:
|
||||
return web.json_response({"item": items[0]})
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "No items returned for imageId",
|
||||
"imageId": image_id,
|
||||
"hint": "This can happen for login-gated images or due to imageId lookup behavior returning empty.",
|
||||
"hint": "This can happen for protected items or when image lookup behavior changes upstream.",
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
@@ -490,8 +763,17 @@ async def civitai_proxy_image(request):
|
||||
if not _is_allowed_image_url(raw_url):
|
||||
return web.json_response({"error": "URL host not allowed"}, status=400)
|
||||
|
||||
page_url = (request.query.get("page_url", "") or "").strip()
|
||||
if page_url and not _is_allowed_page_url(page_url):
|
||||
return web.json_response({"error": "page_url host not allowed"}, status=400)
|
||||
|
||||
try:
|
||||
img_bytes, content_type = await fetch_bytes_authed(raw_url, api_key, timeout_s=60)
|
||||
img_bytes, content_type = await fetch_bytes_authed(
|
||||
raw_url,
|
||||
api_key,
|
||||
timeout_s=60,
|
||||
referer_url=page_url,
|
||||
)
|
||||
return web.Response(
|
||||
body=img_bytes,
|
||||
status=200,
|
||||
@@ -514,4 +796,3 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CivitaiInfoDisplayNode": "CivitAI Info Display",
|
||||
"CivitaiImagePreviewNode": "CivitAI Image Preview",
|
||||
}
|
||||
|
||||
|
||||
+136
-18
@@ -1,10 +1,15 @@
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
console.log("[CivitAI Gallery] frontend script loaded");
|
||||
|
||||
const STORAGE_KEY = "civitai_gallery_simple_v1";
|
||||
const STORAGE_KEY = "civitai_gallery_simple_v2";
|
||||
|
||||
const PAGE_BASES = {
|
||||
com: "https://civitai.com",
|
||||
red: "https://civitai.red",
|
||||
};
|
||||
|
||||
let currentAbort = null;
|
||||
|
||||
// cursor paging state
|
||||
@@ -12,6 +17,42 @@ let nextCursor = null;
|
||||
let nextPageUrl = null;
|
||||
let seenIds = new Set();
|
||||
|
||||
function normalizePageDomain(value) {
|
||||
const v = String(value ?? "").trim().toLowerCase();
|
||||
if (v === "com" || v === "red" || v === "auto") return v;
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function pageBaseFromSetting(value) {
|
||||
const v = normalizePageDomain(value);
|
||||
if (v === "com" || v === "red") return PAGE_BASES[v];
|
||||
return "";
|
||||
}
|
||||
|
||||
function pageBaseFromUrl(raw) {
|
||||
try {
|
||||
if (!raw) return "";
|
||||
const u = new URL(String(raw));
|
||||
const host = (u.hostname || "").toLowerCase();
|
||||
if (
|
||||
host === "civitai.com" ||
|
||||
host === "www.civitai.com" ||
|
||||
host === "civitai.red" ||
|
||||
host === "www.civitai.red"
|
||||
) {
|
||||
return `${u.protocol}//${u.host}`;
|
||||
}
|
||||
} catch {}
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildProxyImageUrl(srcUrl, pageUrl = "") {
|
||||
const proxy = new URL("/civitai_gallery/proxy_image", window.location.origin);
|
||||
proxy.searchParams.set("url", srcUrl ?? "");
|
||||
if (pageUrl) proxy.searchParams.set("page_url", pageUrl);
|
||||
return proxy.toString();
|
||||
}
|
||||
|
||||
// ---------------- Clipboard helper ----------------
|
||||
async function copyText(text) {
|
||||
const t = (text || "").toString().trim();
|
||||
@@ -233,7 +274,7 @@ function isPreviewNode(node) {
|
||||
|
||||
// ---------------- Data helpers ----------------
|
||||
function normalizeItemForSelection(item) {
|
||||
const url = item?.url || item?.imageUrl || item?.src || "";
|
||||
const url = item?.url ?? item?.imageUrl ?? item?.src ?? "";
|
||||
let meta = item?.meta ?? {};
|
||||
let id = item?.id ?? null;
|
||||
let postId = item?.postId ?? null;
|
||||
@@ -246,7 +287,15 @@ function normalizeItemForSelection(item) {
|
||||
if (id != null) id = String(id);
|
||||
if (postId != null) postId = String(postId);
|
||||
|
||||
return { url, meta: meta || {}, id, postId };
|
||||
return {
|
||||
url,
|
||||
meta: meta ?? {},
|
||||
id,
|
||||
postId,
|
||||
pageUrl: item?.pageUrl ?? item?.page_url ?? "",
|
||||
sourcePageUrl: item?.sourcePageUrl ?? item?.source_page_url ?? "",
|
||||
pageDomain: item?.pageDomain ?? item?.page_domain ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function extractPrompts(meta) {
|
||||
@@ -265,9 +314,18 @@ function extractPrompts(meta) {
|
||||
return { positive: positive || "", negative: negative || "" };
|
||||
}
|
||||
|
||||
function buildPageUrl(imageId, postId) {
|
||||
if (imageId) return `https://civitai.com/images/${imageId}`;
|
||||
if (postId) return `https://civitai.com/posts/${postId}`;
|
||||
function buildPageUrl(imageId, postId, options = {}) {
|
||||
const settings = loadSettings();
|
||||
const pageDomain = normalizePageDomain(options.pageDomain ?? settings.pageDomain ?? "auto");
|
||||
|
||||
const forcedBase = pageBaseFromSetting(pageDomain);
|
||||
const explicitBase = pageBaseFromUrl(options.explicitPageUrl);
|
||||
const sourceBase = pageBaseFromUrl(options.sourcePageUrl);
|
||||
|
||||
const base = forcedBase || explicitBase || sourceBase || PAGE_BASES.com;
|
||||
|
||||
if (imageId) return `${base}/images/${imageId}`;
|
||||
if (postId) return `${base}/posts/${postId}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -369,24 +427,24 @@ function setInfoNode(node, text, pageUrl) {
|
||||
}
|
||||
|
||||
async function setPreviewNode(node, srcUrl, pageUrl) {
|
||||
node.__civitaiPageUrl = pageUrl || "";
|
||||
node.__civitaiSourceUrl = srcUrl || "";
|
||||
node.__civitaiPageUrl = pageUrl ?? "";
|
||||
node.__civitaiSourceUrl = srcUrl ?? "";
|
||||
|
||||
await postJSON("/civitai_gallery/set_preview", {
|
||||
node_id: String(node.id),
|
||||
url: node.__civitaiSourceUrl,
|
||||
page_url: node.__civitaiPageUrl,
|
||||
});
|
||||
|
||||
const proxyUrl = `/civitai_gallery/proxy_image?url=${encodeURIComponent(node.__civitaiSourceUrl)}`;
|
||||
node.__civitaiThumbUrl = proxyUrl;
|
||||
const proxyUrl = buildProxyImageUrl(node.__civitaiSourceUrl, node.__civitaiPageUrl);
|
||||
|
||||
node.__civitaiThumbUrl = proxyUrl;
|
||||
if (!node.__civitaiThumbImg) {
|
||||
node.__civitaiThumbImg = new Image();
|
||||
node.__civitaiThumbImg.onload = () => node.graph?.setDirtyCanvas(true, true);
|
||||
node.__civitaiThumbImg.onerror = () => node.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
node.__civitaiThumbImg.src = proxyUrl;
|
||||
|
||||
node.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
@@ -397,7 +455,11 @@ async function updateSupportNodes(normalized) {
|
||||
|
||||
const prompts = extractPrompts(normalized.meta);
|
||||
const model = modelNameFromMeta(normalized.meta);
|
||||
const page = buildPageUrl(normalized.id, normalized.postId);
|
||||
const page = buildPageUrl(normalized.id, normalized.postId, {
|
||||
explicitPageUrl: normalized.pageUrl,
|
||||
sourcePageUrl: normalized.sourcePageUrl,
|
||||
pageDomain: normalized.pageDomain,
|
||||
});
|
||||
|
||||
const infoText =
|
||||
`CivitAI Page: ${page}` +
|
||||
@@ -418,6 +480,7 @@ async function updateSupportNodes(normalized) {
|
||||
|
||||
function applySelection(galleryNode, item) {
|
||||
const normalized = normalizeItemForSelection(item);
|
||||
const selectedPageDomain = normalized.pageDomain || loadSettings().pageDomain || "auto";
|
||||
|
||||
const payload = JSON.stringify({
|
||||
item: {
|
||||
@@ -425,6 +488,9 @@ function applySelection(galleryNode, item) {
|
||||
meta: normalized.meta,
|
||||
id: normalized.id,
|
||||
postId: normalized.postId,
|
||||
pageUrl: normalized.pageUrl,
|
||||
sourcePageUrl: normalized.sourcePageUrl,
|
||||
pageDomain: selectedPageDomain,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -440,7 +506,10 @@ function applySelection(galleryNode, item) {
|
||||
galleryNode.flags = galleryNode.flags || {};
|
||||
galleryNode.flags.dirty = true;
|
||||
|
||||
updateSupportNodes(normalized);
|
||||
updateSupportNodes({
|
||||
...normalized,
|
||||
pageDomain: selectedPageDomain,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- Gallery overlay ----------------
|
||||
@@ -458,9 +527,24 @@ function saveSettings(s) {
|
||||
function loadSettings() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
sort: parsed.sort ?? "Most Reactions",
|
||||
period: parsed.period ?? "AllTime",
|
||||
limit: parsed.limit ?? 36,
|
||||
nsfw: parsed.nsfw ?? "None",
|
||||
pageDomain: normalizePageDomain(parsed.pageDomain ?? "auto"),
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
return { sort: "Most Reactions", period: "AllTime", limit: 36, nsfw: "None" };
|
||||
return {
|
||||
sort: "Most Reactions",
|
||||
period: "AllTime",
|
||||
limit: 36,
|
||||
nsfw: "None",
|
||||
pageDomain: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
function cursorFromNextPage(url) {
|
||||
@@ -519,13 +603,24 @@ async function fetchPage(node, grid, settings, cursor, append = false) {
|
||||
if (itemId) seenIds.add(itemId);
|
||||
if (!item.url) continue;
|
||||
|
||||
const selectedPageDomain = settings.pageDomain || "auto";
|
||||
const normalized = normalizeItemForSelection({
|
||||
...item,
|
||||
pageDomain: item?.pageDomain ?? selectedPageDomain,
|
||||
});
|
||||
const pageUrl = buildPageUrl(normalized.id, normalized.postId, {
|
||||
explicitPageUrl: normalized.pageUrl,
|
||||
sourcePageUrl: normalized.sourcePageUrl,
|
||||
pageDomain: normalized.pageDomain,
|
||||
});
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.style.display = "flex";
|
||||
card.style.flexDirection = "column";
|
||||
card.style.gap = "6px";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = item.url;
|
||||
img.src = buildProxyImageUrl(item.url, pageUrl);
|
||||
img.loading = "lazy";
|
||||
img.style.width = "100%";
|
||||
img.style.borderRadius = "6px";
|
||||
@@ -533,7 +628,11 @@ async function fetchPage(node, grid, settings, cursor, append = false) {
|
||||
|
||||
img.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
applySelection(node, item);
|
||||
applySelection(node, {
|
||||
...item,
|
||||
pageDomain: normalized.pageDomain,
|
||||
pageUrl,
|
||||
});
|
||||
closeFullGallery();
|
||||
};
|
||||
|
||||
@@ -617,6 +716,16 @@ async function openFullGallery(node) {
|
||||
settings.nsfw
|
||||
);
|
||||
|
||||
const pageDomainSel = makeSelect(
|
||||
"Page Links",
|
||||
[
|
||||
["Auto", "auto"],
|
||||
["Civitai.com", "com"],
|
||||
["Civitai.red", "red"],
|
||||
],
|
||||
settings.pageDomain ?? "auto"
|
||||
);
|
||||
|
||||
const searchBtn = document.createElement("button");
|
||||
searchBtn.textContent = "Search";
|
||||
stylizeButton(searchBtn);
|
||||
@@ -631,6 +740,7 @@ async function openFullGallery(node) {
|
||||
periodSel.container,
|
||||
limitInput.container,
|
||||
nsfwSel.container,
|
||||
pageDomainSel.container,
|
||||
searchBtn,
|
||||
closeBtn
|
||||
);
|
||||
@@ -675,6 +785,7 @@ async function openFullGallery(node) {
|
||||
period: periodSel.select.value,
|
||||
limit: parseInt(limitInput.input.value || "36", 10),
|
||||
nsfw: nsfwSel.select.value,
|
||||
pageDomain: pageDomainSel.select.value,
|
||||
};
|
||||
saveSettings(s0);
|
||||
|
||||
@@ -687,6 +798,7 @@ async function openFullGallery(node) {
|
||||
period: periodSel.select.value,
|
||||
limit: parseInt(limitInput.input.value || "36", 10),
|
||||
nsfw: nsfwSel.select.value,
|
||||
pageDomain: pageDomainSel.select.value,
|
||||
};
|
||||
saveSettings(s2);
|
||||
const r = await fetchPage(node, grid, s2, 0, false);
|
||||
@@ -699,6 +811,7 @@ async function openFullGallery(node) {
|
||||
period: periodSel.select.value,
|
||||
limit: parseInt(limitInput.input.value || "36", 10),
|
||||
nsfw: nsfwSel.select.value,
|
||||
pageDomain: pageDomainSel.select.value,
|
||||
};
|
||||
saveSettings(s);
|
||||
|
||||
@@ -751,6 +864,11 @@ async function fetchAndApplyUrl(node, url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
item.sourcePageUrl = trimmed;
|
||||
if (!item.pageUrl) item.pageUrl = trimmed;
|
||||
} catch {}
|
||||
|
||||
applySelection(node, item);
|
||||
} catch (e) {
|
||||
console.warn("[CivitAI Gallery] node fetch URL failed", e);
|
||||
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
[project]
|
||||
name = "comfyui-civitai-imageplus"
|
||||
version = "0.1.2"
|
||||
description = "Browse CivitAI images in ComfyUI, extract and edit prompts, preview selected image and pass to I2I."
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"numpy",
|
||||
"Pillow",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "covertbannana"
|
||||
DisplayName = "CivitAI ImagePlus"
|
||||
Icon = ""
|
||||
|
||||
|
||||
[project]
|
||||
name = "comfyui-civitai-imageplus"
|
||||
version = "0.2.0"
|
||||
description = "Browse CivitAI images in ComfyUI, preview protected images, edit prompts, and pass selected images into img2img workflows."
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"numpy",
|
||||
"Pillow",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus"
|
||||
Documentation = "https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus#readme"
|
||||
"Bug Tracker" = "https://github.com/CovertBannana/ComfyUI_CivitAI_ImagePlus/issues"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "covertbannana"
|
||||
DisplayName = "CivitAI ImagePlus"
|
||||
Icon = ""
|
||||
|
||||
Reference in New Issue
Block a user