Files
MajoorWaldi-ComfyUI-Majoor-…/js/api/endpoints.js
T

513 lines
17 KiB
JavaScript

/**
* API Endpoints - Centralized URL constants
*/
import { normalizeAssetId, pickRootId } from "../utils/ids.js";
import { hasWindowsDrivePrefix, splitFilenameAndSubfolder, toPosixPath } from "../utils/path.js";
const DEFAULT_QUERY_LIMIT = 200;
const DEFAULT_QUERY_OFFSET = 0;
export const ENDPOINTS = {
// Health & Config
HEALTH: "/mjr/am/health",
HEALTH_COUNTERS: "/mjr/am/health/counters",
HEALTH_DB: "/mjr/am/health/db",
STATUS: "/mjr/am/status",
RUNTIME_EXECUTION: "/mjr/am/runtime/execution",
CONFIG: "/mjr/am/config",
VERSION: "/mjr/am/version",
// Index & Search
SCAN: "/mjr/am/scan",
INDEX_FILES: "/mjr/am/index-files",
INDEX_RESET: "/mjr/am/index/reset",
DB_RESET: "/mjr/am/db/reset",
SEARCH: "/mjr/am/search",
LIST: "/mjr/am/list",
ROOTS: "/mjr/am/roots",
CUSTOM_ROOTS: "/mjr/am/custom-roots",
CUSTOM_ROOTS_REMOVE: "/mjr/am/custom-roots/remove",
BROWSER_FOLDER_OP: "/mjr/am/browser/folder-op",
BROWSE_FOLDER: "/mjr/sys/browse-folder",
FOLDER_INFO: "/mjr/am/folder-info",
// Metadata
METADATA: "/mjr/am/metadata",
WORKFLOW_QUICK: "/mjr/am/workflow-quick",
RETRY_SERVICES: "/mjr/am/retry-services",
STAGE_TO_INPUT: "/mjr/am/stage-to-input",
OPEN_IN_FOLDER: "/mjr/am/open-in-folder",
TOOLS_STATUS: "/mjr/am/tools/status",
SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory",
SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory",
SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback",
SETTINGS_VECTOR_SEARCH: "/mjr/am/settings/vector-search",
SETTINGS_EXECUTION_GROUPING: "/mjr/am/settings/execution-grouping",
SETTINGS_HUGGINGFACE: "/mjr/am/settings/huggingface",
SETTINGS_AI_LOGGING: "/mjr/am/settings/ai-logging",
SETTINGS_ROUTE_LOGGING: "/mjr/am/settings/route-logging",
SETTINGS_STARTUP_LOGGING: "/mjr/am/settings/startup-logging",
SETTINGS_LTXAV_RGB_FALLBACK: "/mjr/am/settings/ltxav-rgb-fallback",
// View (ComfyUI native)
VIEW: "/view",
// Custom roots view (Majoor)
CUSTOM_VIEW: "/mjr/am/custom-view",
// Viewer helpers (Majoor)
VIEWER_INFO: "/mjr/am/viewer/info",
VIEWER_RESOURCE: "/mjr/am/viewer/resource",
// File upload
UPLOAD_INPUT: "/mjr/am/upload_input",
// File download
DOWNLOAD: "/mjr/am/download",
DOWNLOAD_CLEAN: "/mjr/am/download-clean",
// Drag-out (OS) helpers
BATCH_ZIP_CREATE: "/mjr/am/batch-zip",
// Calendar helpers
DATE_HISTOGRAM: "/mjr/am/date-histogram",
// Asset operations
ASSET_DELETE: "/mjr/am/asset/delete",
ASSET_RENAME: "/mjr/am/asset/rename",
// Watcher
WATCHER_STATUS: "/mjr/am/watcher/status",
WATCHER_TOGGLE: "/mjr/am/watcher/toggle",
WATCHER_SCOPE: "/mjr/am/watcher/scope",
WATCHER_SETTINGS: "/mjr/am/watcher/settings",
// Collections
COLLECTIONS: "/mjr/am/collections",
// Stacks
STACKS: "/mjr/am/stacks",
STACKS_MERGE: "/mjr/am/stacks/merge",
STACKS_DISSOLVE: "/mjr/am/stacks/dissolve",
STACKS_AUTO_STACK: "/mjr/am/stacks/auto-stack",
// Duplicate detection
DUPLICATES_ALERTS: "/mjr/am/duplicates/alerts",
DUPLICATES_ANALYZE: "/mjr/am/duplicates/analyze",
DUPLICATES_MERGE_TAGS: "/mjr/am/duplicates/merge-tags",
// DB backup/restore
DB_BACKUPS: "/mjr/am/db/backups",
DB_BACKUP_SAVE: "/mjr/am/db/backup-save",
DB_BACKUP_RESTORE: "/mjr/am/db/backup-restore",
// Vector / Semantic search
VECTOR_SEARCH: "/mjr/am/vector/search",
VECTOR_SIMILAR: "/mjr/am/vector/similar",
VECTOR_ALIGNMENT: "/mjr/am/vector/alignment",
VECTOR_AUTO_TAGS: "/mjr/am/vector/auto-tags",
VECTOR_CAPTION: "/mjr/am/vector/caption",
// Legacy alias kept for backward compatibility.
VECTOR_ENHANCED_PROMPT: "/mjr/am/vector/enhanced-prompt",
VECTOR_INDEX: "/mjr/am/vector/index",
VECTOR_STATS: "/mjr/am/vector/stats",
VECTOR_SUGGEST_COLLECTIONS: "/mjr/am/vector/suggest-collections",
VECTOR_BACKFILL: "/mjr/am/db/backfill-missing-vectors",
VECTOR_BACKFILL_STATUS: "/mjr/am/db/backfill-missing-vectors/status",
// Hybrid search (FTS + semantic)
HYBRID_SEARCH: "/mjr/am/search/hybrid",
// Library audit
AUDIT: "/mjr/am/audit",
};
export function appendAssetFilterQueryParams(url, filters = {}) {
let nextUrl = String(url || "");
// Use correct separator based on whether URL already has query params
const _sep = () => {
const s = nextUrl.includes("?") ? "&" : "?";
return s;
};
const _append = (param) => {
nextUrl += _sep() + param;
};
const {
subfolder = null,
kind = null,
hasWorkflow = null,
minRating = null,
minSizeMB = null,
maxSizeMB = null,
minWidth = null,
minHeight = null,
maxWidth = null,
maxHeight = null,
workflowType = null,
dateRange = null,
dateExact = null,
} = filters || {};
if (subfolder) {
_append(`subfolder=${encodeURIComponent(String(subfolder))}`);
}
if (kind) {
_append(`kind=${encodeURIComponent(kind)}`);
}
if (hasWorkflow !== null && hasWorkflow !== undefined) {
_append(`has_workflow=${encodeURIComponent(hasWorkflow ? "true" : "false")}`);
}
if (minRating !== null && minRating !== undefined && Number(minRating) > 0) {
_append(`min_rating=${encodeURIComponent(String(minRating))}`);
}
if (minSizeMB !== null && minSizeMB !== undefined && Number(minSizeMB) > 0) {
_append(`min_size_mb=${encodeURIComponent(String(minSizeMB))}`);
}
if (maxSizeMB !== null && maxSizeMB !== undefined && Number(maxSizeMB) > 0) {
_append(`max_size_mb=${encodeURIComponent(String(maxSizeMB))}`);
}
if (minWidth !== null && minWidth !== undefined && Number(minWidth) > 0) {
_append(`min_width=${encodeURIComponent(String(minWidth))}`);
}
if (minHeight !== null && minHeight !== undefined && Number(minHeight) > 0) {
_append(`min_height=${encodeURIComponent(String(minHeight))}`);
}
if (maxWidth !== null && maxWidth !== undefined && Number(maxWidth) > 0) {
_append(`max_width=${encodeURIComponent(String(maxWidth))}`);
}
if (maxHeight !== null && maxHeight !== undefined && Number(maxHeight) > 0) {
_append(`max_height=${encodeURIComponent(String(maxHeight))}`);
}
if (workflowType) {
_append(`workflow_type=${encodeURIComponent(String(workflowType))}`);
}
if (dateRange) {
_append(`date_range=${encodeURIComponent(String(dateRange))}`);
}
if (dateExact) {
_append(`date_exact=${encodeURIComponent(String(dateExact))}`);
}
return nextUrl;
}
/**
* Build view URL for asset
*/
export function buildViewURL(filename, subfolder = null, type = "output") {
const fn = String(filename || "").trim();
if (!fn) return "";
let url = `${ENDPOINTS.VIEW}?filename=${encodeURIComponent(fn)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
url += `&type=${encodeURIComponent(type)}`;
return url;
}
/**
* Build list URL with scope support.
*/
export function buildListURL(params = {}) {
const {
q = "*",
limit = DEFAULT_QUERY_LIMIT,
offset = DEFAULT_QUERY_OFFSET,
scope = "output",
subfolder = "",
customRootId = null,
kind = null,
hasWorkflow = null,
minRating = null,
minSizeMB = null,
maxSizeMB = null,
resolutionCompare = null,
minWidth = null,
minHeight = null,
maxWidth = null,
maxHeight = null,
workflowType = null,
dateRange = null,
dateExact = null,
sort = null,
includeTotal = true,
groupStacks = false,
} = params;
let url = `${ENDPOINTS.LIST}?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}&scope=${encodeURIComponent(scope)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
if (customRootId) {
url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
} else if (String(scope || "").toLowerCase() === "custom") {
// Browser scope without selected root uses absolute filesystem paths.
url += "&browser_mode=1";
}
url = appendAssetFilterQueryParams(url, {
kind,
hasWorkflow,
minRating,
minSizeMB,
maxSizeMB,
minWidth,
minHeight,
maxWidth,
maxHeight,
workflowType,
dateRange,
dateExact,
});
if (resolutionCompare) {
url += `&resolution_compare=${encodeURIComponent(String(resolutionCompare))}`;
}
if (sort) {
url += `&sort=${encodeURIComponent(String(sort))}`;
}
if (includeTotal === false) {
url += `&include_total=0`;
}
if (groupStacks) {
url += "&group_stacks=1";
}
return url;
}
export function buildCustomViewURL(filename, subfolder = "", rootId = "") {
const fn = String(filename || "").trim();
const rid = String(rootId || "").trim();
if (!fn || !rid) return "";
let url = `${ENDPOINTS.CUSTOM_VIEW}?root_id=${encodeURIComponent(rid)}&filename=${encodeURIComponent(fn)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
return url;
}
export function buildBatchZipDownloadURL(token) {
return `${ENDPOINTS.BATCH_ZIP_CREATE}/${encodeURIComponent(String(token || ""))}`;
}
export function buildStacksURL({ limit = 50, offset = 0, includeTotal = true } = {}) {
let url = `${ENDPOINTS.STACKS}?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`;
if (includeTotal === false) url += "&include_total=0";
return url;
}
export function buildStackURL(stackId) {
return `${ENDPOINTS.STACKS}/${encodeURIComponent(String(stackId || ""))}`;
}
export function buildStackMembersURL(stackId) {
return `${buildStackURL(stackId)}/members`;
}
export function buildStackByJobURL(jobId) {
return `${ENDPOINTS.STACKS}/by-job/${encodeURIComponent(String(jobId || ""))}`;
}
export function buildStackCoverURL(stackId) {
return `${buildStackURL(stackId)}/cover`;
}
export function buildStackRenameURL(stackId) {
return `${buildStackURL(stackId)}/rename`;
}
export function buildViewerResourceURL(assetOrContext, relpath = "") {
const ctx = assetOrContext && typeof assetOrContext === "object" ? assetOrContext : {};
const rel = String(relpath || "").trim();
if (!rel) return "";
const assetId = normalizeAssetId(ctx?.asset_id ?? ctx?.id ?? null);
let url = `${ENDPOINTS.VIEWER_RESOURCE}?relpath=${encodeURIComponent(rel)}`;
if (assetId) {
return `${url}&asset_id=${encodeURIComponent(assetId)}`;
}
const filepath = toPosixPath(
String(
ctx?.filepath ||
ctx?.path ||
ctx?.fullpath ||
ctx?.full_path ||
ctx?.file_info?.filepath ||
ctx?.file_info?.path ||
"",
).trim(),
);
if (filepath) {
return `${url}&filepath=${encodeURIComponent(filepath)}`;
}
const filename = String(ctx?.filename || ctx?.name || ctx?.file_info?.filename || "").trim();
if (!filename) return "";
url += `&filename=${encodeURIComponent(filename)}`;
const subfolder = String(ctx?.subfolder || ctx?.file_info?.subfolder || "").trim();
if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;
const type = String(ctx?.type || ctx?.file_info?.type || "")
.trim()
.toLowerCase();
if (type && type !== "custom") {
url += `&type=${encodeURIComponent(type)}`;
}
const rootId = String(pickRootId(ctx) || ctx?.root_id || ctx?.custom_root_id || "").trim();
if (rootId) {
url += `&root_id=${encodeURIComponent(rootId)}`;
}
return url;
}
export function buildDateHistogramURL(params = {}) {
const {
scope = "output",
customRootId = null,
month = "",
kind = null,
hasWorkflow = null,
minRating = null,
} = params;
let url = `${ENDPOINTS.DATE_HISTOGRAM}?scope=${encodeURIComponent(scope)}&month=${encodeURIComponent(String(month || ""))}`;
if (customRootId) {
url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
}
return appendAssetFilterQueryParams(url, {
subfolder: params.subfolder ?? null,
kind,
hasWorkflow,
minRating,
minSizeMB: params.minSizeMB ?? null,
maxSizeMB: params.maxSizeMB ?? null,
minWidth: params.minWidth ?? null,
minHeight: params.minHeight ?? null,
maxWidth: params.maxWidth ?? null,
maxHeight: params.maxHeight ?? null,
workflowType: params.workflowType ?? null,
dateRange: params.dateRange ?? null,
dateExact: params.dateExact ?? null,
});
}
export function buildAssetViewURL(asset) {
const mtime = asset?.mtime;
const withMtime = (url) => {
if (!url || !mtime) return url;
return `${url}${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(mtime)}`;
};
const rawPath = toPosixPath(
String(
asset?.filepath ||
asset?.path ||
asset?.fullpath ||
asset?.full_path ||
asset?.file_info?.filepath ||
asset?.file_info?.path ||
"",
).trim(),
);
let filename = String(
asset?.filename || asset?.name || asset?.file_info?.filename || "",
).trim();
let subfolder = String(asset?.subfolder || asset?.file_info?.subfolder || "").trim();
const pickFromPath = (p) => {
const out = { type: "", subfolder: "", filename: "" };
const normalized = toPosixPath(p);
if (!normalized) return out;
const idxOut = normalized.toLowerCase().indexOf("/output/");
const idxIn = normalized.toLowerCase().indexOf("/input/");
let baseIdx = -1;
if (idxOut >= 0) {
out.type = "output";
baseIdx = idxOut + "/output/".length;
} else if (idxIn >= 0) {
out.type = "input";
baseIdx = idxIn + "/input/".length;
}
if (baseIdx >= 0) {
const rel = normalized.slice(baseIdx);
const slash = rel.lastIndexOf("/");
if (slash >= 0) {
out.subfolder = rel.slice(0, slash);
out.filename = rel.slice(slash + 1);
} else {
out.filename = rel;
}
} else {
const split = splitFilenameAndSubfolder(normalized);
out.filename = split.filename;
}
return out;
};
const fromPath = pickFromPath(rawPath);
// If filename accidentally contains a relative path, split it for /view API.
if (!subfolder && filename.includes("/")) {
const idx = filename.lastIndexOf("/");
if (idx > 0) {
subfolder = filename.slice(0, idx);
filename = filename.slice(idx + 1);
}
}
if (!filename && fromPath.filename) filename = fromPath.filename;
if (!subfolder && fromPath.subfolder) subfolder = fromPath.subfolder;
if (!filename) return "";
let type = String(asset?.type || asset?.file_info?.type || "")
.toLowerCase()
.trim();
if (type !== "input" && type !== "output" && type !== "custom") type = "";
if (!type && fromPath.type) type = fromPath.type;
if (!type && rawPath) {
if (rawPath.includes("/input/")) type = "input";
else if (rawPath.includes("/output/")) type = "output";
}
if (!type) type = "output";
const hasNativeBucket = rawPath.includes("/output/") || rawPath.includes("/input/");
const subfolderLooksAbsolute = hasWindowsDrivePrefix(subfolder) || subfolder.startsWith("/");
// Fallback: non-native output/input roots (or broken absolute subfolder values)
// cannot be served by ComfyUI `/view`, so use backend filepath streaming URL.
if (rawPath && type !== "custom" && (subfolderLooksAbsolute || !hasNativeBucket)) {
return withMtime(buildDownloadURL(rawPath, { inline: true }));
}
if (type === "custom") {
const rid = String(pickRootId(asset) || "").trim();
if (rid) return withMtime(buildCustomViewURL(filename, subfolder, rid));
if (rawPath) {
return withMtime(
`${ENDPOINTS.CUSTOM_VIEW}?filepath=${encodeURIComponent(rawPath)}&browser_mode=1`,
);
}
// Fallback for malformed custom assets without root id.
const fallbackType = fromPath.type || "output";
return withMtime(buildViewURL(filename, subfolder, fallbackType));
}
// Prefer path-based type when explicit type conflicts with obvious filepath bucket.
if (rawPath.includes("/output/")) type = "output";
if (rawPath.includes("/input/")) type = "input";
return withMtime(buildViewURL(filename, subfolder, type));
}
/**
* Build download URL for asset
*/
export function buildDownloadURL(filepath, options = {}) {
if (!filepath) return "";
const inline = !!options?.inline;
let url = `${ENDPOINTS.DOWNLOAD}?filepath=${encodeURIComponent(filepath)}`;
if (inline) url += "&preview=1";
return url;
}
/**
* Build download URL for asset with ComfyUI metadata stripped.
*/
export function buildCleanDownloadURL(filepath) {
if (!filepath) return "";
return `${ENDPOINTS.DOWNLOAD_CLEAN}?filepath=${encodeURIComponent(filepath)}`;
}