Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ docs/
logs/
standalone_tagger.py
prompts.db
prompts.db-shm
prompts.db-wal
prompts.db-journal

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
62 changes: 62 additions & 0 deletions database/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def search_prompts(
rating_max: Optional[int] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
folder: Optional[str] = None,
limit: int = 100,
offset: int = 0,
) -> List[Dict[str, Any]]:
Expand All @@ -201,6 +202,7 @@ def search_prompts(
rating_max: Maximum rating filter
date_from: Start date filter (ISO format)
date_to: End date filter (ISO format)
folder: Filter by subfolder name in generated image paths
limit: Maximum number of results
offset: Number of results to skip

Expand All @@ -227,6 +229,26 @@ def search_prompts(
)
params.append(tag)

if folder:
# Escape LIKE wildcards in the folder name
safe_folder = (
folder.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
)
query_parts.append(
"AND prompts.id IN ("
" SELECT DISTINCT prompt_id FROM generated_images"
" WHERE image_path LIKE ? ESCAPE '\\'"
" OR image_path LIKE ? ESCAPE '\\'"
" OR image_path LIKE ? ESCAPE '\\'"
")"
)
# Pattern 1: folder between forward slashes (Unix + normalized Windows)
params.append(f"%/{safe_folder}/%")
# Pattern 2: folder between backslashes (Windows native)
params.append(f"%\\\\{safe_folder}\\\\%")
# Pattern 3: folder at start of a relative path
params.append(f"{safe_folder}/%")

if rating_min is not None:
query_parts.append("AND rating >= ?")
params.append(rating_min)
Expand Down Expand Up @@ -435,6 +457,46 @@ def get_all_categories(self) -> List[str]:
)
return [row["category"] for row in cursor.fetchall()]

def get_prompt_subfolders(self, root_dirs: Optional[List[str]] = None) -> List[str]:
"""
Get distinct subfolder paths from generated_images.

Extracts the directory portion of image_path, made relative to
root_dirs if provided. Returns sorted unique folder names.
"""
with self.model.get_connection() as conn:
cursor = conn.execute(
"SELECT DISTINCT image_path FROM generated_images "
"WHERE image_path IS NOT NULL AND image_path != ''"
)

folders = set()
for row in cursor.fetchall():
image_path = row["image_path"]
parent = os.path.dirname(image_path)
if not parent:
continue

made_relative = False
if root_dirs:
for root in root_dirs:
try:
rel = os.path.relpath(parent, root)
if not rel.startswith(".."):
if rel != ".":
folders.add(rel)
made_relative = True
break
except ValueError:
continue

if not made_relative:
basename = os.path.basename(parent)
if basename:
folders.add(basename)

return sorted(folders)

def get_all_tags(self) -> List[str]:
"""
Get all unique tags that are in use (linked to at least one prompt).
Expand Down
98 changes: 62 additions & 36 deletions py/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ def __init__(self):
self.db = PromptDatabase()
self._cached_output_dir = None # Lazy-cached by _find_comfyui_output_dir()
self._html_cache = {} # Cached HTML file contents keyed by path
self._gallery_cache = None # Cached gallery file listing (Fix 2.4)
self._gallery_cache_time = 0 # Timestamp of last cache fill
self._gallery_cache = {} # dict: path_str -> (files, timestamp)
self._gallery_cache_ttl = 30 # Cache TTL in seconds

# Run cleanup on initialization to remove any existing duplicates
Expand Down Expand Up @@ -156,8 +155,7 @@ def invalidate_gallery_cache(self):

Called by the image monitor when new files are detected.
"""
self._gallery_cache = None
self._gallery_cache_time = 0
self._gallery_cache = {}

def add_routes(self, routes):
"""Register all API routes with the ComfyUI server.
Expand Down Expand Up @@ -391,47 +389,52 @@ async def serve_js_static(request):

# ── Shared utilities used by multiple mixins ──────────────────────

def _enrich_prompt_images(self, prompts):
"""Add url and thumbnail_url to each image in prompt results."""
def _enrich_images(self, images):
"""Add url and thumbnail_url to a flat list of image dicts."""
from urllib.parse import quote as url_quote

output_dir = self._find_comfyui_output_dir()
output_path = Path(output_dir) if output_dir else None
output_dirs = self._get_all_output_dirs()

for prompt in prompts:
for image in prompt.get("images", []):
image_path_str = image.get("image_path", "")
if not image_path_str:
continue
for image in images:
image_path_str = image.get("image_path", "")
if not image_path_str:
continue

img_path = Path(image_path_str)
img_path = Path(image_path_str)

# Set fallback url via image ID
if image.get("id"):
image["url"] = f"/prompt_manager/images/{image['id']}/file"
# Set fallback url via image ID
if image.get("id"):
image["url"] = f"/prompt_manager/images/{image['id']}/file"

# Try to compute relative path and thumbnail URL
if output_path:
try:
rel_path = img_path.resolve().relative_to(output_path.resolve())
image["relative_path"] = str(rel_path)
image["url"] = (
f"/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe='/')}"
)
# Try each root to compute relative path and thumbnail URL
for output_path in output_dirs:
try:
rel_path = img_path.resolve().relative_to(output_path.resolve())
image["relative_path"] = str(rel_path)
image["url"] = (
f"/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe='/')}"
)

# Check for thumbnail
rel_no_ext = rel_path.with_suffix("")
thumb_rel = (
f"thumbnails/{rel_no_ext.as_posix()}_thumb{rel_path.suffix}"
# Check for thumbnail
rel_no_ext = rel_path.with_suffix("")
thumb_rel = (
f"thumbnails/{rel_no_ext.as_posix()}_thumb{rel_path.suffix}"
)
thumb_abs = output_path / thumb_rel
if thumb_abs.exists():
image["thumbnail_url"] = (
f"/prompt_manager/images/serve/{url_quote(thumb_rel, safe='/')}"
)
thumb_abs = output_path / thumb_rel
if thumb_abs.exists():
image["thumbnail_url"] = (
f"/prompt_manager/images/serve/{url_quote(thumb_rel, safe='/')}"
)
except (ValueError, RuntimeError):
pass
break # Found matching root, stop searching
except (ValueError, RuntimeError):
continue # Try next root

return images

def _enrich_prompt_images(self, prompts):
"""Add url and thumbnail_url to each image in prompt results."""
for prompt in prompts:
self._enrich_images(prompt.get("images", []))
return prompts

def _clean_nan_recursive(self, obj):
Expand Down Expand Up @@ -550,6 +553,29 @@ def _find_comfyui_output_dir(self):

return None

def _get_all_output_dirs(self):
"""Get all configured output directories, falling back to auto-detect.

Returns:
List[Path]: Valid output directory paths, possibly empty.
"""
from ..config import GalleryConfig

output_dirs = []
if GalleryConfig.MONITORING_DIRECTORIES:
for d in GalleryConfig.MONITORING_DIRECTORIES:
p = Path(d).resolve()
if p.is_dir():
output_dirs.append(p)

# Fallback to auto-detect if no configured dirs are valid
if not output_dirs:
fallback = self._find_comfyui_output_dir()
if fallback:
output_dirs.append(Path(fallback))

return output_dirs

def _extract_comfyui_metadata(self, image_path):
"""Extract ComfyUI workflow metadata from PNG image files."""
try:
Expand Down
Loading
Loading