File path refacto

This commit is contained in:
Ewald
2026-05-21 21:07:16 +02:00
parent ddfe817b45
commit 120c822c6f
31 changed files with 6414 additions and 6207 deletions
+45
View File
@@ -179,6 +179,14 @@ export function registerSidebarTab(tabDef) {
}
}
export function registerSidebarTabForApp(app, tabDef) {
try {
return registerSidebarTabCompat(app || _app || getComfyApp(), tabDef);
} catch {
return false;
}
}
/**
* Register a bottom panel tab.
*
@@ -193,6 +201,14 @@ export function registerBottomPanelTab(tabDef) {
}
}
export function registerBottomPanelTabForApp(app, tabDef) {
try {
return registerBottomPanelTabCompat(app || _app || getComfyApp(), tabDef);
} catch {
return false;
}
}
/**
* Programmatically activate a sidebar tab.
*
@@ -207,6 +223,14 @@ export function activateSidebarTab(tabId) {
}
}
export function activateSidebarTabForApp(app, tabId) {
try {
return activateSidebarTabCompat(app || _app || getComfyApp(), tabId);
} catch {
return false;
}
}
// ── commands & keybindings ────────────────────────────────────────────────────
/**
@@ -223,6 +247,14 @@ export function registerCommand(commandDef) {
}
}
export function registerCommandForApp(app, commandDef) {
try {
return registerCommandCompat(app || _app || getComfyApp(), commandDef);
} catch {
return false;
}
}
/**
* Register a keybinding.
*
@@ -237,6 +269,14 @@ export function registerKeybinding(keybindingDef) {
}
}
export function registerKeybindingForApp(app, keybindingDef) {
try {
return registerKeybindingCompat(app || _app || getComfyApp(), keybindingDef);
} catch {
return false;
}
}
// ── extension manager (escape hatch) ─────────────────────────────────────────
/**
@@ -311,10 +351,15 @@ export const hostAdapter = {
getSetting,
getHostSettingsApi,
registerSidebarTab,
registerSidebarTabForApp,
registerBottomPanelTab,
registerBottomPanelTabForApp,
activateSidebarTab,
activateSidebarTabForApp,
registerCommand,
registerCommandForApp,
registerKeybinding,
registerKeybindingForApp,
getHostExtensionManager,
getRawHostApp,
getRawHostApi,
+11 -11
View File
@@ -17,12 +17,12 @@
import { runStartupWarmup } from "../../app/bootstrap.js";
import {
activateSidebarTabCompat,
registerBottomPanelTabCompat,
registerCommandCompat,
registerKeybindingCompat,
registerSidebarTabCompat,
} from "../../app/comfyApiBridge.js";
activateSidebarTabForApp,
registerBottomPanelTabForApp,
registerCommandForApp,
registerKeybindingForApp,
registerSidebarTabForApp,
} from "../../app/hostAdapter.js";
import { EVENTS } from "../../app/events.js";
import { openMajoorSettings } from "../../app/openMajoorSettings.js";
import { comfyToast } from "../../app/toast.js";
@@ -91,7 +91,7 @@ export { teardownTopBarMfvButton };
// ── sidebar helpers ───────────────────────────────────────────────────────────
export function openAssetsManagerPanel(runtimeApp, sidebarTabId) {
const opened = activateSidebarTabCompat(runtimeApp, sidebarTabId);
const opened = activateSidebarTabForApp(runtimeApp, sidebarTabId);
if (!opened) {
try {
window.dispatchEvent(new Event(EVENTS.OPEN_ASSETS_MANAGER));
@@ -281,13 +281,13 @@ export function buildAboutPageBadges() {
/** Imperatively registers commands — used as a fallback for older ComfyUI. */
export function registerNativeCommands(runtimeApp, options) {
for (const command of buildNativeCommands(runtimeApp, options)) {
registerCommandCompat(runtimeApp, command);
registerCommandForApp(runtimeApp, command);
}
}
export function registerNativeKeybindings(runtimeApp) {
for (const keybinding of buildNativeKeybindings()) {
registerKeybindingCompat(runtimeApp, keybinding);
registerKeybindingForApp(runtimeApp, keybinding);
}
}
@@ -303,7 +303,7 @@ export function registerNativeKeybindings(runtimeApp) {
* Full teardown only happens when the extension is unregistered.
*/
export function registerAssetsSidebar(runtimeApp, { sidebarTabId }) {
return registerSidebarTabCompat(runtimeApp, {
return registerSidebarTabForApp(runtimeApp, {
id: sidebarTabId,
icon: "pi pi-folder",
title: t("manager.title"),
@@ -415,7 +415,7 @@ export function buildBottomPanelTabs() {
}
export function registerGeneratedBottomPanel(runtimeApp) {
return registerBottomPanelTabCompat(runtimeApp, getGeneratedFeedBottomPanelTab());
return registerBottomPanelTabForApp(runtimeApp, getGeneratedFeedBottomPanelTab());
}
export function teardownGeneratedFeed() {
@@ -0,0 +1,49 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";
import SidebarFileInfoSection from "../vue/components/panel/sidebar/SidebarFileInfoSection.vue";
describe("SidebarFileInfoSection", () => {
it("shows execution provenance fields from top-level asset data", () => {
const wrapper = mount(SidebarFileInfoSection, {
props: {
asset: {
id: 9,
job_id: "prompt-1",
source_node_id: "7",
source_node_type: "SaveImage",
},
},
});
const text = wrapper.text();
expect(text).toContain("Job ID");
expect(text).toContain("prompt-1");
expect(text).toContain("Source Node");
expect(text).toContain("7");
expect(text).toContain("Node Type");
expect(text).toContain("SaveImage");
wrapper.unmount();
});
it("shows execution provenance fields from nested file_info", () => {
const wrapper = mount(SidebarFileInfoSection, {
props: {
asset: {
file_info: {
job_id: "prompt-2",
source_node_id: "12",
source_node_type: "VHS_VideoCombine",
},
},
},
});
const text = wrapper.text();
expect(text).toContain("prompt-2");
expect(text).toContain("12");
expect(text).toContain("VHS_VideoCombine");
wrapper.unmount();
});
});
@@ -31,6 +31,10 @@ function isAnimatedMedia(asset) {
}
}
function readAssetField(asset, key) {
return asset?.[key] ?? asset?.file_info?.[key] ?? "";
}
const rows = computed(() => {
const asset = props.asset || {};
const fileData = [];
@@ -87,8 +91,17 @@ const rows = computed(() => {
if (asset.id != null) {
fileData.push({ label: "Asset ID", value: String(asset.id), tooltip: "Internal database asset identifier" });
}
if (asset.job_id) {
fileData.push({ label: "Job ID", value: String(asset.job_id), tooltip: "Workflow execution job identifier (prompt_id)" });
const jobId = String(readAssetField(asset, "job_id") || "").trim();
if (jobId) {
fileData.push({ label: "Job ID", value: jobId, tooltip: "Workflow execution job identifier (prompt_id)" });
}
const sourceNodeId = String(readAssetField(asset, "source_node_id") || "").trim();
if (sourceNodeId) {
fileData.push({ label: "Source Node", value: sourceNodeId, tooltip: "ComfyUI node id that produced this file" });
}
const sourceNodeType = String(readAssetField(asset, "source_node_type") || "").trim();
if (sourceNodeType) {
fileData.push({ label: "Node Type", value: sourceNodeType, tooltip: "ComfyUI node class that produced this file" });
}
return fileData;
});
@@ -121,7 +121,12 @@ function hasFileInfoData(asset) {
timestamp ||
(target.size && target.size > 0) ||
target.id != null ||
target.job_id,
target.job_id ||
target.file_info?.job_id ||
target.source_node_id ||
target.file_info?.source_node_id ||
target.source_node_type ||
target.file_info?.source_node_type,
);
}
@@ -1,5 +1,5 @@
import { p as Be, M as Je, c as $e, q as tn, t as en, u as nn, v as sn, A as gt, x as Q, y as on, z as rn, B as an, C as ce, D as Le, F as ln, G as R, H as Et, I as cn, J as Ie, K as dn, L as un, N as Nt, E as dt, O as pn, n as mn, s as kt, Q as hn, R as fn, S as Pt, T as _n, U as Ut, V as Yt, d as Ft, W as gn, l as bn, X as yn } from "./entry-GhzfvnC5.js";
import { ensureViewerMetadataAsset as Lt } from "./genInfo-BgI38YPf.js";
import { p as Be, M as Je, c as $e, q as tn, t as en, u as nn, v as sn, A as gt, x as Q, y as on, z as rn, B as an, C as ce, D as Le, F as ln, G as R, H as Et, I as cn, J as Ie, K as dn, L as un, N as Nt, E as dt, O as pn, n as mn, s as kt, Q as hn, R as fn, S as Pt, T as _n, U as Ut, V as Yt, d as Ft, W as gn, l as bn, X as yn } from "./entry-B5M8pQrP.js";
import { ensureViewerMetadataAsset as Lt } from "./genInfo-h4muYyLg.js";
const L = Object.freeze({
SIMPLE: "simple",
AB: "ab",
@@ -1,4 +1,4 @@
import { E as g, w as E, e as l } from "./entry-GhzfvnC5.js";
import { E as g, w as E, e as l } from "./entry-B5M8pQrP.js";
let p = !1, i = null, u = null, c = null, d = null, w = null, v = 0, _ = 0;
const f = 400, M = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif", ".bmp"]), S = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"]), L = /* @__PURE__ */ new Set([".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".opus"]), I = /* @__PURE__ */ new Set([".glb", ".gltf", ".obj", ".fbx", ".stl", ".usdz"]);
function T(e) {
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import { _ as nn, j as on, k as tn, A as B, l as en, m as rn, o as cn } from "./entry-GhzfvnC5.js";
import { _ as nn, j as on, k as tn, A as B, l as en, m as rn, o as cn } from "./entry-B5M8pQrP.js";
import { o as E, c as R, t as $, a as O, b as I, n as ln, d as z, e as j } from "./mjr-vue-vendor-LmyMh4Y4.js";
const un = { style: { display: "flex", "flex-direction": "column", gap: "10px", "margin-bottom": "14px" } }, fn = {
key: 0,
@@ -12,7 +12,7 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
}, yn = {
key: 7,
style: { border: "1px solid rgba(255,255,255,0.10)", "border-radius": "10px", background: "rgba(255,255,255,0.04)", overflow: "hidden" }
}, an = { style: { margin: "0", padding: "10px 12px", "max-height": "280px", overflow: "auto", "font-size": "11px", "line-height": "1.35", color: "rgba(255,255,255,0.86)" } }, _n = {
}, _n = { style: { margin: "0", padding: "10px 12px", "max-height": "280px", overflow: "auto", "font-size": "11px", "line-height": "1.35", color: "rgba(255,255,255,0.86)" } }, an = {
__name: "ViewerMetadataBlock",
props: {
title: { type: String, default: "" },
@@ -39,8 +39,8 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
const y = r.trim();
if (!y) return null;
try {
const _ = JSON.parse(y);
return _ && typeof _ == "object" ? _ : null;
const a = JSON.parse(y);
return a && typeof a == "object" ? a : null;
} catch {
return null;
}
@@ -50,8 +50,8 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
const r = Object.entries(i || {});
if (!r.length) return !1;
let y = 0;
for (const [, _] of r.slice(0, 50))
if (!(!_ || typeof _ != "object") && (_.inputs && typeof _.inputs == "object" && (y += 1), y >= 2))
for (const [, a] of r.slice(0, 50))
if (!(!a || typeof a != "object") && (a.inputs && typeof a.inputs == "object" && (y += 1), y >= 2))
return !0;
} catch {
return !1;
@@ -63,10 +63,10 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
if (!y) return null;
if (typeof y == "object") return y;
if (typeof y != "string") return null;
const _ = y.trim();
if (!_) return null;
const a = y.trim();
if (!a) return null;
try {
return JSON.parse(_);
return JSON.parse(a);
} catch {
return null;
}
@@ -76,10 +76,10 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
if (!y) return null;
if (typeof y == "object") return u(y) ? y : null;
if (typeof y != "string") return null;
const _ = y.trim();
if (!_) return null;
const a = y.trim();
if (!a) return null;
try {
const f = JSON.parse(_);
const f = JSON.parse(a);
return u(f) ? f : null;
} catch {
return null;
@@ -89,10 +89,11 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
return !!(d(i) || l(i));
}
function g(i) {
var a, f, h;
const r = i || {}, y = r.generation_time || r.file_creation_time || r.mtime || r.created_at;
return !!(r.width && r.height || r.duration && r.duration > 0 || y || r.size && r.size > 0 || r.id != null || r.job_id);
return !!(r.width && r.height || r.duration && r.duration > 0 || y || r.size && r.size > 0 || r.id != null || r.job_id || (a = r.file_info) != null && a.job_id || r.source_node_id || (f = r.file_info) != null && f.source_node_id || r.source_node_type || (h = r.file_info) != null && h.source_node_type);
}
function a(i) {
function _(i) {
if (i == null) return "";
const r = typeof i == "string" ? i : JSON.stringify(i, null, 2);
return r ? r.length > 4e4 ? `${r.slice(0, 4e4)}
@@ -105,17 +106,17 @@ const un = { style: { display: "flex", "flex-direction": "column", gap: "10px",
() => B.WORKFLOW_MINIMAP_ENABLED !== !1 && o(t.asset)
), N = j(
() => b.value && typeof b.value == "object" && b.value.kind === "fetch_error"
), x = j(() => {
), k = j(() => {
var i;
return a((i = t.asset) == null ? void 0 : i.metadata_raw);
}), h = j(
return _((i = t.asset) == null ? void 0 : i.metadata_raw);
}), x = j(
() => !t.loading && !N.value && !T.value && !G.value && !m.value
), S = j(() => {
var y, _, f, k;
var y, a, f, h;
if (!N.value) return "";
const i = String(
((y = b.value) == null ? void 0 : y.message) || ((_ = b.value) == null ? void 0 : _.error) || "Failed to load generation data."
), r = String(((f = b.value) == null ? void 0 : f.code) || ((k = b.value) == null ? void 0 : k.stage) || "").trim();
((y = b.value) == null ? void 0 : y.message) || ((a = b.value) == null ? void 0 : a.error) || "Failed to load generation data."
), r = String(((f = b.value) == null ? void 0 : f.code) || ((h = b.value) == null ? void 0 : h.stage) || "").trim();
return r ? `${i}
Code: ${r}
@@ -149,10 +150,10 @@ Click to retry.`;
key: 5,
asset: t.asset
}, null, 8, ["asset"])) : O("", !0),
h.value ? (E(), R("div", gn, " No generation data found for this file. ")) : O("", !0),
x.value ? (E(), R("details", yn, [
x.value ? (E(), R("div", gn, " No generation data found for this file. ")) : O("", !0),
k.value ? (E(), R("details", yn, [
r[2] || (r[2] = I("summary", { style: { cursor: "pointer", padding: "10px 12px", color: "rgba(255,255,255,0.78)", "user-select": "none" } }, " Raw metadata ", -1)),
I("pre", an, $(x.value), 1)
I("pre", _n, $(k.value), 1)
])) : O("", !0)
]));
}
@@ -169,23 +170,23 @@ const P = ((U = B) == null ? void 0 : U.VIEWER_GENINFO_MAX_ENTRIES) ?? 300, V =
var c, u;
try {
const d = Date.now();
for (const [g, a] of n.entries()) {
if (!a) {
for (const [g, _] of n.entries()) {
if (!_) {
n.delete(g);
continue;
}
d - (a.at || 0) > t && n.delete(g);
d - (_.at || 0) > t && n.delete(g);
}
if (n.size <= e) return;
const l = Array.from(n.entries()).sort(
(g, a) => {
(g, _) => {
var p, b;
return (((p = g == null ? void 0 : g[1]) == null ? void 0 : p.at) || 0) - (((b = a == null ? void 0 : a[1]) == null ? void 0 : b.at) || 0);
return (((p = g == null ? void 0 : g[1]) == null ? void 0 : p.at) || 0) - (((b = _ == null ? void 0 : _[1]) == null ? void 0 : b.at) || 0);
}
), o = n.size - e;
for (let g = 0; g < o; g++) {
const a = (c = l[g]) == null ? void 0 : c[0];
a != null && n.delete(a);
const _ = (c = l[g]) == null ? void 0 : c[0];
_ != null && n.delete(_);
}
} catch (d) {
(u = console.debug) == null || u.call(console, d);
@@ -332,7 +333,7 @@ const P = ((U = B) == null ? void 0 : U.VIEWER_GENINFO_MAX_ENTRIES) ?? 300, V =
(t = console.debug) == null || t.call(console, e);
}
}, kn = (n, t) => {
var c, u, d, l, o, g, a;
var c, u, d, l, o, g, _;
const e = t && typeof t == "object" ? t : null;
if (!e) return n;
try {
@@ -378,7 +379,7 @@ const P = ((U = B) == null ? void 0 : U.VIEWER_GENINFO_MAX_ENTRIES) ?? 300, V =
}
}
} catch (p) {
(a = console.debug) == null || a.call(console, p);
(_ = console.debug) == null || _.call(console, p);
}
return n;
};
@@ -390,10 +391,10 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
const g = l ? D(V, l, A) : null;
if (g && typeof g == "object")
return { ...n, ...g };
const a = l ? D(W, l, M) : null;
if (a) {
const _ = l ? D(W, l, M) : null;
if (_) {
try {
J(o, a);
J(o, _);
} catch (m) {
(b = console.debug) == null || b.call(console, m);
}
@@ -408,23 +409,23 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
(v = console.debug) == null || v.call(console, m);
}
const p = async () => {
var S, i, r, y, _;
var S, i, r, y, a;
const m = d != null ? L(() => {
var f, k;
return ((k = (f = c == null ? void 0 : c.getCached) == null ? void 0 : f.call(c, d)) == null ? void 0 : k.data) || null;
var f, h;
return ((h = (f = c == null ? void 0 : c.getCached) == null ? void 0 : f.call(c, d)) == null ? void 0 : h.data) || null;
}, null) : null;
m && typeof m == "object" && (o = { ...n, ...m });
const N = !!(o != null && o.has_generation_data || o != null && o.has_workflow || o != null && o.has_generation || o != null && o.has_generation_info), x = !!(o != null && o.geninfo || o != null && o.prompt || o != null && o.workflow || o != null && o.metadata);
let h = null;
if (d != null && (!F(o) || N && !x)) {
const N = !!(o != null && o.has_generation_data || o != null && o.has_workflow || o != null && o.has_generation || o != null && o.has_generation_info), k = !!(o != null && o.geninfo || o != null && o.prompt || o != null && o.workflow || o != null && o.metadata);
let x = null;
if (d != null && (!F(o) || N && !k)) {
const f = await L(
() => t == null ? void 0 : t(d, u ? { signal: u } : {}),
null
);
f != null && f.ok && f.data && typeof f.data == "object" ? (o = { ...o, ...f.data }, L(() => {
var k;
return (k = c == null ? void 0 : c.setCached) == null ? void 0 : k.call(c, d, f.data);
})) : f && (f == null ? void 0 : f.code) !== "ABORTED" && (h = {
var h;
return (h = c == null ? void 0 : c.setCached) == null ? void 0 : h.call(c, d, f.data);
})) : f && (f == null ? void 0 : f.code) !== "ABORTED" && (x = {
kind: "fetch_error",
stage: "asset",
code: (f == null ? void 0 : f.code) || "FETCH_ERROR",
@@ -433,7 +434,7 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
}
if (!F(o))
try {
const f = String((o == null ? void 0 : o.source) || (o == null ? void 0 : o.type) || "output").trim().toLowerCase() || "output", k = String(
const f = String((o == null ? void 0 : o.source) || (o == null ? void 0 : o.type) || "output").trim().toLowerCase() || "output", h = String(
(o == null ? void 0 : o.filename) || (o == null ? void 0 : o.name) || ((S = o == null ? void 0 : o.file_info) == null ? void 0 : S.filename) || ""
).trim(), Y = String(
(o == null ? void 0 : o.subfolder) || ((i = o == null ? void 0 : o.file_info) == null ? void 0 : i.subfolder) || ""
@@ -442,12 +443,12 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
).trim(), s = String(
(o == null ? void 0 : o.filepath) || (o == null ? void 0 : o.path) || ((y = o == null ? void 0 : o.file_info) == null ? void 0 : y.filepath) || ""
).trim();
if (k) {
if (h) {
const w = await L(
() => e == null ? void 0 : e(
{
type: f,
filename: k,
filename: h,
subfolder: Y,
root_id: Z,
filepath: s
@@ -456,7 +457,7 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
),
null
);
w != null && w.ok && w.data ? o = kn({ ...o }, w.data) : w && (w == null ? void 0 : w.code) !== "ABORTED" && (h = {
w != null && w.ok && w.data ? o = kn({ ...o }, w.data) : w && (w == null ? void 0 : w.code) !== "ABORTED" && (x = {
kind: "fetch_error",
stage: "file_scoped",
code: (w == null ? void 0 : w.code) || "FETCH_ERROR",
@@ -464,40 +465,40 @@ async function jn(n, { getAssetMetadata: t, getFileMetadataScoped: e, metadataCa
});
}
} catch (f) {
(_ = console.debug) == null || _.call(console, f);
(a = console.debug) == null || a.call(console, f);
}
if (xn(o), !F(o) && h) {
if (xn(o), !F(o) && x) {
const f = wn(o);
f && f.kind === "media_pipeline" || J(o, h);
f && f.kind === "media_pipeline" || J(o, x);
}
return F(o) && l ? H(V, l, o, A, P) : h && l && H(
return F(o) && l ? H(V, l, o, A, P) : x && l && H(
W,
l,
h,
x,
M,
P
), o;
};
if (l) {
const m = () => {
var x;
var k;
try {
C.delete(l);
} catch (h) {
(x = console.debug) == null || x.call(console, h);
} catch (x) {
(k = console.debug) == null || k.call(console, x);
}
};
try {
(G = u == null ? void 0 : u.addEventListener) == null || G.call(u, "abort", m, { once: !0 });
} catch (x) {
(T = console.debug) == null || T.call(console, x);
} catch (k) {
(T = console.debug) == null || T.call(console, k);
}
const N = p().finally(() => {
var x, h, S;
var k, x, S;
try {
(x = u == null ? void 0 : u.removeEventListener) == null || x.call(u, "abort", m);
(k = u == null ? void 0 : u.removeEventListener) == null || k.call(u, "abort", m);
} catch (i) {
(h = console.debug) == null || h.call(console, i);
(x = console.debug) == null || x.call(console, i);
}
try {
C.delete(l);
@@ -513,7 +514,7 @@ function Nn({ title: n, asset: t, ui: e } = {}) {
var l;
const c = document.createElement("div");
try {
const { app: o } = cn(_n, {
const { app: o } = cn(an, {
title: n,
asset: t,
loading: !!(e != null && e.loading),
@@ -523,8 +524,8 @@ function Nn({ title: n, asset: t, ui: e } = {}) {
var g;
try {
o.unmount();
} catch (a) {
(g = console.debug) == null || g.call(console, a);
} catch (_) {
(g = console.debug) == null || g.call(console, _);
}
}, c;
} catch (o) {
@@ -541,11 +542,11 @@ function Nn({ title: n, asset: t, ui: e } = {}) {
o.style.cssText = "border: 1px solid rgba(255,255,255,0.10); border-radius: 10px; background: rgba(255,255,255,0.04); overflow: hidden;";
const g = document.createElement("summary");
g.textContent = "Raw metadata", g.style.cssText = "cursor: pointer; padding: 10px 12px; color: rgba(255,255,255,0.78); user-select: none;";
const a = document.createElement("pre");
a.style.cssText = "margin:0; padding: 10px 12px; max-height: 280px; overflow:auto; font-size: 11px; line-height: 1.35; color: rgba(255,255,255,0.86);";
const _ = document.createElement("pre");
_.style.cssText = "margin:0; padding: 10px 12px; max-height: 280px; overflow:auto; font-size: 11px; line-height: 1.35; color: rgba(255,255,255,0.86);";
let p = typeof d == "string" ? d : JSON.stringify(d, null, 2);
p.length > 4e4 && (p = `${p.slice(0, 4e4)}
...(truncated)`), a.textContent = p, o.appendChild(g), o.appendChild(a), u.appendChild(o);
...(truncated)`), _.textContent = p, o.appendChild(g), o.appendChild(_), u.appendChild(o);
}
return u;
}
@@ -1,4 +1,4 @@
import { M as s, a as o, P as D, b as t, c as M, f as d, g as l, i as E, d as r, r as i } from "./entry-GhzfvnC5.js";
import { M as s, a as o, P as D, b as t, c as M, f as d, g as l, i as E, d as r, r as i } from "./entry-B5M8pQrP.js";
export {
s as MODEL3D_EXTS,
o as MODEL3D_EXT_TO_LOADER,
@@ -1,4 +1,4 @@
import { h as C } from "./entry-GhzfvnC5.js";
import { h as C } from "./entry-B5M8pQrP.js";
function H(t, n, o) {
return 0.2126 * t + 0.7152 * n + 0.0722 * o;
}
@@ -1,4 +1,4 @@
import { i as q, n as Y, s as k } from "./entry-GhzfvnC5.js";
import { i as q, n as Y, s as k } from "./entry-B5M8pQrP.js";
function Z(o) {
if (!o) return null;
try {
+1 -1
View File
@@ -1,2 +1,2 @@
import "./chunks/entry-GhzfvnC5.js";
import "./chunks/entry-B5M8pQrP.js";
import "../../scripts/app.js";
+82
View File
@@ -176,6 +176,88 @@ async def fetch_by_job_id(job_id: str) -> list[CoreAssetInfo]:
return []
def _clean_sync_tags(tags: list[str] | tuple[str, ...] | None) -> list[str]:
out: list[str] = []
seen: set[str] = set()
for tag in tags or []:
text = str(tag or "").strip()
if not text:
continue
key = text.casefold()
if key in seen:
continue
seen.add(key)
out.append(text)
return out
async def _asset_filepath_by_id(db: Any, asset_id: int) -> str:
try:
res = await db.aquery("SELECT filepath FROM assets WHERE id = ?", (int(asset_id),))
if res.ok and res.data:
return str((res.data[0] or {}).get("filepath") or "").strip()
except Exception as exc:
logger.debug("Core asset sync filepath lookup failed for asset %s: %s", asset_id, exc)
return ""
async def sync_user_metadata_by_asset_id(
db: Any,
asset_id: int,
*,
rating: int | None = None,
tags: list[str] | tuple[str, ...] | None = None,
metadata: dict[str, Any] | None = None,
) -> bool:
"""Best-effort write-through of Majoor metadata into Comfy core assets."""
if not is_available():
return False
filepath = await _asset_filepath_by_id(db, asset_id)
if not filepath:
return False
info = await fetch_by_path(filepath)
if not info:
return False
payload: dict[str, Any] = {}
if rating is not None:
payload["rating"] = max(0, min(5, int(rating or 0)))
if tags is not None:
payload["tags"] = _clean_sync_tags(tags)
if metadata:
payload["metadata"] = dict(metadata)
if not payload:
return False
try:
from app.assets import services as asset_services # type: ignore
except Exception as exc:
logger.debug("Core asset services unavailable for metadata sync: %s", exc)
return False
for name in (
"update_asset_user_metadata",
"update_asset_metadata",
"update_asset",
"set_asset_metadata",
):
fn = getattr(asset_services, name, None)
if not callable(fn):
continue
for args, kwargs in (
((info.reference_id,), payload),
((), {"asset_id": info.reference_id, **payload}),
((), {"reference_id": info.reference_id, **payload}),
):
try:
result = fn(*args, **kwargs)
if hasattr(result, "__await__"):
await result
return True
except Exception as exc:
logger.debug("Core asset metadata sync via %s failed: %s", name, exc)
return False
def _paths_equal(a: str, b: str) -> bool:
"""Case-insensitive, separator-normalised path comparison."""
import os
+2 -2
View File
@@ -374,9 +374,9 @@ def _resolve_builtin_roots() -> tuple[Path | None, Path | None]:
except Exception:
output_root = None
try:
import folder_paths
from mjr_am_backend.adapters.comfy_core import get_input_directory
input_root = Path(folder_paths.get_input_directory()).resolve()
input_root = Path(str(get_input_directory() or "")).resolve()
except Exception:
input_root = None
return output_root, input_root
@@ -5,21 +5,14 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from ...adapters.comfy_core import get_input_directory as get_comfy_input_directory
from ...shared import Result
from ..index.scan_batch_utils import normalize_filepath_str
try:
import folder_paths # type: ignore
except Exception: # pragma: no cover
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
folder_paths = SimpleNamespace(get_input_directory=get_comfy_input_directory)
async def load_asset_filepath_by_id(services: dict[str, Any], asset_id: int) -> Result[str]:
@@ -325,7 +318,7 @@ async def resolve_or_create_asset_id(
normalize_path: Callable[[str], Path | None],
is_within_root: Callable[[Path, Path], bool],
get_runtime_output_root_fn: Callable[[], str],
get_input_directory: Callable[[], str] | None = None,
get_input_directory: Callable[[], str | None] | None = None,
list_custom_roots_fn: Callable[[], Result[Any]],
resolve_custom_root_fn: Callable[[str], Result[Any]],
safe_error_message: Callable[[Exception, str], str],
@@ -348,10 +341,10 @@ async def resolve_or_create_asset_id(
source = _normalize_asset_source(file_type)
if get_input_directory is None:
get_input_directory = folder_paths.get_input_directory
get_input_directory = get_comfy_input_directory
out_root = Path(get_runtime_output_root_fn()).resolve(strict=False)
in_root = Path(get_input_directory()).resolve(strict=False)
in_root = Path(str(get_input_directory() or "")).resolve(strict=False)
source, base_dir, resolved_root_id = _resolve_asset_root(
resolved,
source=source,
@@ -391,7 +384,7 @@ async def infer_source_and_root_id_from_path(
*,
is_within_root: Callable[[Path, Path], bool],
get_runtime_output_root_fn: Callable[[], str],
get_input_directory: Callable[[], str] | None = None,
get_input_directory: Callable[[], str | None] | None = None,
list_custom_roots_fn: Callable[[], Result[Any]],
logger: Any,
) -> tuple[str, str | None]:
@@ -400,9 +393,9 @@ async def infer_source_and_root_id_from_path(
except Exception:
out_root = Path(get_runtime_output_root_fn()).resolve(strict=False)
if get_input_directory is None:
get_input_directory = folder_paths.get_input_directory
get_input_directory = get_comfy_input_directory
try:
in_root = Path(get_input_directory()).resolve(strict=False)
in_root = Path(str(get_input_directory() or "")).resolve(strict=False)
except Exception:
in_root = (Path(__file__).resolve().parents[3] / "input").resolve(strict=False)
@@ -437,4 +430,4 @@ __all__ = [
"load_asset_filepath_by_id",
"load_asset_row_by_id",
"resolve_or_create_asset_id",
]
]
@@ -958,7 +958,7 @@ class MetadataHelpers:
same_quality = _metadata_quality_is_equal_sql()
replace_raw_on_equal = _metadata_raw_should_replace_on_equal_quality_sql()
return await db.aexecute(
result = await db.aexecute(
f"""
INSERT INTO asset_metadata
(
@@ -1053,6 +1053,23 @@ class MetadataHelpers:
asset_id,
),
)
if result.ok:
try:
from ...adapters.core_assets import sync_user_metadata_by_asset_id
await sync_user_metadata_by_asset_id(
db,
asset_id,
metadata={
"metadata_quality": metadata_quality,
"workflow_type": workflow_type,
"generation_time_ms": generation_time_ms,
"positive_prompt": positive_prompt,
},
)
except Exception:
pass
return result
@staticmethod
async def refresh_metadata_if_needed(
+3
View File
@@ -4,6 +4,7 @@ Asset updater - handles rating and tag updates.
import json
from typing import Any
from ...adapters.core_assets import sync_user_metadata_by_asset_id
from ...adapters.db.sqlite import Sqlite
from ...shared import Result
@@ -67,6 +68,7 @@ class AssetUpdater:
if not result.ok:
return Result.Err("UPDATE_FAILED", result.error or "Failed to update rating")
await sync_user_metadata_by_asset_id(self.db, asset_id, rating=rating)
return Result.Ok({"asset_id": asset_id, "rating": rating})
async def update_asset_tags(self, asset_id: int, tags: list[str]) -> Result[dict[str, Any]]:
@@ -95,6 +97,7 @@ class AssetUpdater:
if not result.ok:
return Result.Err("UPDATE_FAILED", result.error or "Failed to update tags")
await sync_user_metadata_by_asset_id(self.db, asset_id, tags=sanitized)
return Result.Ok({"asset_id": asset_id, "tags": sanitized})
def _sanitize_tags(self, tags: list[str]) -> list[str]:
@@ -2,9 +2,11 @@
Asset management endpoints: ratings, tags, service retry.
"""
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from aiohttp import web
from mjr_am_backend.adapters.comfy_core import get_input_directory as _get_comfy_input_directory
from mjr_am_backend.config import TO_THREAD_TIMEOUT_S, get_runtime_output_root
from mjr_am_backend.custom_roots import list_custom_roots, resolve_custom_root
from mjr_am_backend.features.assets import (
@@ -45,9 +47,6 @@ from mjr_am_backend.features.assets.delete_service import (
from mjr_am_backend.features.assets.filename_validator import (
validate_filename as _validate_filename,
)
from mjr_am_backend.features.assets.lookup_service import (
folder_paths,
)
from mjr_am_backend.features.assets.lookup_service import (
infer_source_and_root_id_from_path as _infer_source_and_root_id,
)
@@ -81,6 +80,11 @@ from ..core import (
)
logger = get_logger(__name__)
folder_paths = SimpleNamespace(get_input_directory=_get_comfy_input_directory)
def get_input_directory() -> str:
return str(folder_paths.get_input_directory())
MAX_TAGS_PER_ASSET = 50
MAX_TAG_LENGTH = 100
@@ -170,7 +174,7 @@ def _wired_resolve_or_create(**kwargs: Any) -> Any:
normalize_path=_normalize_path,
is_within_root=_is_within_root,
get_runtime_output_root_fn=get_runtime_output_root,
get_input_directory=folder_paths.get_input_directory,
get_input_directory=get_input_directory,
list_custom_roots_fn=list_custom_roots,
resolve_custom_root_fn=resolve_custom_root,
safe_error_message=_safe_error_message,
@@ -382,7 +386,7 @@ def register_asset_routes(routes: web.RouteTableDef) -> None:
infer_source_and_root_id_from_path=_infer_source_and_root_id,
is_within_root=_is_within_root,
get_runtime_output_root=get_runtime_output_root,
get_input_directory=folder_paths.get_input_directory,
get_input_directory=get_input_directory,
list_custom_roots=list_custom_roots,
to_thread_timeout_s=TO_THREAD_TIMEOUT_S,
safe_error_message=_safe_error_message,
+2 -8
View File
@@ -21,6 +21,7 @@ from pathlib import Path
from typing import Any
from aiohttp import web
from mjr_am_backend.adapters.comfy_core import get_input_directory
from mjr_am_backend.config import OUTPUT_ROOT_PATH
from mjr_am_backend.custom_roots import resolve_custom_root
from mjr_am_backend.features.assets.download_service import (
@@ -39,11 +40,6 @@ from mjr_am_backend.routes.core.security import (
from mjr_am_backend.routes.core.services import _require_services
from mjr_am_backend.shared import Result, get_logger, sanitize_error_message
try:
import folder_paths # type: ignore
except Exception:
folder_paths = None # type: ignore
logger = get_logger(__name__)
_BATCH_DIR = Path(
@@ -365,9 +361,7 @@ def _resolve_base_dir(item: dict[str, Any], typ: str) -> Path | None:
def _resolve_input_base_dir() -> Path | None:
try:
if folder_paths is None:
return None
return Path(str(folder_paths.get_input_directory())).resolve(strict=True)
return Path(str(get_input_directory())).resolve(strict=True)
except Exception:
return None
@@ -641,8 +641,9 @@ def register_db_maintenance_routes(routes: web.RouteTableDef) -> None:
except Exception:
pass
try:
import folder_paths # type: ignore
input_path = str(Path(folder_paths.get_input_directory()).resolve())
from mjr_am_backend.adapters.comfy_core import get_input_directory
input_path = str(Path(str(get_input_directory() or "")).resolve())
_spawn_background_task(
index_service.scan_directory(
input_path,
@@ -789,8 +790,9 @@ def register_db_maintenance_routes(routes: web.RouteTableDef) -> None:
except Exception:
pass
try:
import folder_paths # type: ignore
input_path = str(Path(folder_paths.get_input_directory()).resolve())
from mjr_am_backend.adapters.comfy_core import get_input_directory
input_path = str(Path(str(get_input_directory() or "")).resolve())
_spawn_background_task(
index_service.scan_directory(
input_path,
@@ -1379,8 +1381,9 @@ def register_db_maintenance_routes(routes: web.RouteTableDef) -> None:
except Exception:
pass
try:
import folder_paths # type: ignore
input_path = str(Path(folder_paths.get_input_directory()).resolve())
from mjr_am_backend.adapters.comfy_core import get_input_directory
input_path = str(Path(str(get_input_directory() or "")).resolve())
_spawn_background_task(
index_service.scan_directory(
input_path,
+4 -14
View File
@@ -10,17 +10,7 @@ from pathlib import Path
from aiohttp import web
from mjr_am_backend.adapters.comfy_core import get_capabilities as get_comfy_core_capabilities
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
from mjr_am_backend.adapters.comfy_core import get_input_directory
from mjr_am_backend.config import (
EXECUTION_IDLE_GRACE_SECONDS,
MEDIA_PROBE_BACKEND,
@@ -494,11 +484,11 @@ def register_health_routes(routes: web.RouteTableDef) -> None:
if scope == "output":
roots = [out_root]
elif scope == "input":
roots = [str(Path(folder_paths.get_input_directory()).resolve(strict=False))]
roots = [str(Path(get_input_directory()).resolve(strict=False))]
elif scope == "all":
roots = [
out_root,
str(Path(folder_paths.get_input_directory()).resolve(strict=False)),
str(Path(get_input_directory()).resolve(strict=False)),
]
elif scope == "custom":
if not _is_valid_custom_root_id(custom_root_id):
@@ -1725,7 +1715,7 @@ def register_health_routes(routes: web.RouteTableDef) -> None:
roots = {
"output_directory": await _runtime_output_root(svc),
"input_directory": str(Path(folder_paths.get_input_directory()).resolve()),
"input_directory": str(Path(get_input_directory()).resolve()),
}
custom = list_custom_roots()
+8 -14
View File
@@ -2,8 +2,10 @@
Metadata extraction endpoint.
"""
import asyncio
from types import SimpleNamespace
from aiohttp import web
from mjr_am_backend.adapters.comfy_core import get_input_directory as _get_comfy_input_directory
from mjr_am_backend.config import TO_THREAD_TIMEOUT_S, get_runtime_output_root
from mjr_am_backend.custom_roots import resolve_custom_root
from mjr_am_backend.shared import Result, sanitize_error_message
@@ -18,21 +20,13 @@ from ..core import (
_safe_rel_path,
)
try:
import folder_paths # type: ignore
except Exception:
from pathlib import Path
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
_METADATA_RATE_LIMIT_MAX_REQUESTS = 10
_METADATA_RATE_LIMIT_WINDOW_SECONDS = 60
folder_paths = SimpleNamespace(get_input_directory=_get_comfy_input_directory)
def get_input_directory() -> str:
return str(folder_paths.get_input_directory())
def register_metadata_routes(routes: web.RouteTableDef) -> None:
@@ -91,7 +85,7 @@ def register_metadata_routes(routes: web.RouteTableDef) -> None:
# Resolve base root
if file_type == "input":
base_root = folder_paths.get_input_directory()
base_root = get_input_directory()
elif file_type == "custom":
if not root_id:
return _json_response(Result.Err("INVALID_INPUT", "Missing custom root_id"))
+14 -18
View File
@@ -4,20 +4,11 @@ Directory scanning and file indexing endpoints.
import asyncio
import time
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from aiohttp import web
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
from mjr_am_backend.adapters.comfy_core import get_input_directory as _get_comfy_input_directory
from mjr_am_backend.adapters.db.schema import purge_orphan_vec_embeddings, rebuild_fts
from mjr_am_backend.config import (
COLLECTIONS_DIR_PATH,
@@ -92,6 +83,11 @@ from .scan_watcher import (
)
logger = get_logger(__name__)
folder_paths = SimpleNamespace(get_input_directory=_get_comfy_input_directory)
def get_input_directory() -> str:
return str(folder_paths.get_input_directory())
def _send_prompt_event(event: str, payload: Any) -> None:
@@ -337,7 +333,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
scan_source = "output"
custom_root_id = None
elif scope in ("input", "inputs"):
directory = folder_paths.get_input_directory()
directory = get_input_directory()
scan_source = "input"
custom_root_id = None
elif scope == "custom":
@@ -351,7 +347,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
all_scope_result = await run_all_scope_scan(
services=svc,
output_root=output_root,
input_root=folder_paths.get_input_directory(),
input_root=get_input_directory(),
recursive=payload.recursive,
incremental=payload.incremental,
fast=payload.fast,
@@ -395,7 +391,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
base_root_res = _resolve_scan_root(str(output_root))
allowed = base_root_res.ok and _is_within_root(normalized_dir, base_root_res.data)
elif scan_source == "input":
base_root_res = _resolve_scan_root(str(folder_paths.get_input_directory()))
base_root_res = _resolve_scan_root(str(get_input_directory()))
allowed = base_root_res.ok and _is_within_root(normalized_dir, base_root_res.data)
elif scan_source == "custom":
root_result = resolve_custom_root(str(custom_root_id or scan_root_id or ""))
@@ -407,7 +403,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
else:
# No explicit scope: allow only within configured output/input or registered custom roots.
out_root = _resolve_scan_root(str(output_root))
in_root = _resolve_scan_root(str(folder_paths.get_input_directory()))
in_root = _resolve_scan_root(str(get_input_directory()))
allowed = (
(out_root.ok and _is_within_root(normalized_dir, out_root.data))
or (in_root.ok and _is_within_root(normalized_dir, in_root.data))
@@ -581,7 +577,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
except Exception:
out_root = None
try:
in_root = Path(folder_paths.get_input_directory()).resolve(strict=True)
in_root = Path(get_input_directory()).resolve(strict=True)
except Exception:
in_root = None
@@ -643,7 +639,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
continue
if file_type == "input":
base_root = folder_paths.get_input_directory()
base_root = get_input_directory()
elif file_type == "custom":
root_result = resolve_custom_root(str(root_id or ""))
if not root_result.ok:
@@ -1332,7 +1328,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None:
scope=scope,
custom_root_id=custom_root_id,
output_root=output_root,
input_root=folder_paths.get_input_directory(),
input_root=get_input_directory(),
resolve_custom_root=resolve_custom_root,
)
if not target_roots_res.ok:
@@ -394,15 +394,9 @@ def _resolve_scan_root(directory: str) -> Result[Path]:
def _resolve_input_directory() -> Path:
try:
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
return Path(folder_paths.get_input_directory()).resolve()
from mjr_am_backend.adapters.comfy_core import get_input_directory
return Path(str(get_input_directory() or "")).resolve()
except Exception:
return (Path(__file__).parent.parent.parent.parent / "input").resolve()
+4 -14
View File
@@ -9,17 +9,7 @@ from pathlib import Path
from typing import Any
from aiohttp import web
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
from mjr_am_backend.adapters.comfy_core import get_input_directory
from mjr_am_backend.config import TO_THREAD_TIMEOUT_S
from mjr_am_backend.custom_roots import resolve_custom_root
from mjr_am_backend.shared import Result, get_logger, sanitize_error_message
@@ -114,7 +104,7 @@ def register_staging_routes(routes: web.RouteTableDef, *, deps: dict | None = No
_runtime_output_root_ = _d.get("_runtime_output_root", _runtime_output_root) if _d is not None else _runtime_output_root
_resolve_stage_destination_ = _d.get("_resolve_stage_destination", _resolve_stage_destination) if _d is not None else _resolve_stage_destination
_resolve_custom_root_ = _d.get("resolve_custom_root", resolve_custom_root) if _d is not None else resolve_custom_root
_folder_paths_ = _d.get("folder_paths", folder_paths) if _d is not None else folder_paths
_get_input_directory_ = _d.get("get_input_directory", get_input_directory) if _d is not None else get_input_directory
_is_path_allowed_ = _d.get("_is_path_allowed", _is_path_allowed) if _d is not None else _is_path_allowed
_schedule_index_task_ = _d.get("_schedule_index_task", _schedule_index_task) if _d is not None else _schedule_index_task
_audit_log_write_ = _d.get("audit_log_write", audit_log_write) if _d is not None else audit_log_write
@@ -165,7 +155,7 @@ def register_staging_routes(routes: web.RouteTableDef, *, deps: dict | None = No
await _audit_stage(result, requested_files=0, purpose=purpose, index=bool(index_staged))
return _json_response(result)
input_root = Path(_folder_paths_.get_input_directory()).resolve()
input_root = Path(_get_input_directory_()).resolve()
output_root = await _runtime_output_root_(svc)
staged = []
staged_paths = []
@@ -219,7 +209,7 @@ def register_staging_routes(routes: web.RouteTableDef, *, deps: dict | None = No
continue
if file_type == "input":
base_root = _folder_paths_.get_input_directory()
base_root = _get_input_directory_()
elif file_type == "custom":
root_result = _resolve_custom_root_(str(root_id or ""))
if not root_result.ok:
+3 -13
View File
@@ -4,17 +4,7 @@ Upload handler for the /mjr/am/upload_input endpoint.
from pathlib import Path
from aiohttp import web
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
from mjr_am_backend.adapters.comfy_core import get_input_directory
from mjr_am_backend.shared import Result, get_logger
from ..core import (
@@ -77,7 +67,7 @@ def register_upload_routes(routes: web.RouteTableDef, *, deps: dict | None = Non
_require_services_ = _d.get("_require_services", _require_services) if _d is not None else _require_services
_write_multipart_file_atomic_ = _d.get("_write_multipart_file_atomic", _write_multipart_file_atomic) if _d is not None else _write_multipart_file_atomic
_schedule_index_task_ = _d.get("_schedule_index_task", _schedule_index_task) if _d is not None else _schedule_index_task
_folder_paths_ = _d.get("folder_paths", folder_paths) if _d is not None else folder_paths
_get_input_directory_ = _d.get("get_input_directory", get_input_directory) if _d is not None else get_input_directory
_audit_log_write_ = _d.get("audit_log_write", audit_log_write) if _d is not None else audit_log_write
async def _audit_upload(result: Result, **details: object) -> None:
@@ -127,7 +117,7 @@ def register_upload_routes(routes: web.RouteTableDef, *, deps: dict | None = Non
skip_index = _upload_skip_index(request)
input_dir = Path(_folder_paths_.get_input_directory()).resolve()
input_dir = Path(_get_input_directory_()).resolve()
write_res = await _write_multipart_file_atomic_(input_dir, filename, field)
if not write_res.ok:
+8 -13
View File
@@ -1,21 +1,11 @@
"""
Search and list endpoints.
"""
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from aiohttp import web
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
from mjr_am_backend.adapters.comfy_core import get_input_directory as _get_comfy_input_directory
from mjr_am_backend.config import TO_THREAD_TIMEOUT_S
from mjr_am_backend.custom_roots import resolve_custom_root
from mjr_am_backend.features.index.metadata_helpers import MetadataHelpers
@@ -64,6 +54,11 @@ AUTOCOMPLETE_RATE_LIMIT_MAX_REQUESTS = 40
AUTOCOMPLETE_RATE_LIMIT_WINDOW_SECONDS = 60
logger = get_logger(__name__)
folder_paths = SimpleNamespace(get_input_directory=_get_comfy_input_directory)
def get_input_directory() -> str:
return str(folder_paths.get_input_directory())
def _safe_error_message_for_base_exception(exc: BaseException, fallback: str) -> str:
@@ -190,7 +185,7 @@ async def list_assets(request: web.Request) -> web.Response:
normalize_sort_key=_normalize_sort_key,
require_services=_require_services,
touch_enrichment_pause=_touch_enrichment_pause,
get_input_directory=folder_paths.get_input_directory,
get_input_directory=get_input_directory,
kickoff_background_scan=_kickoff_background_scan,
list_filesystem_assets=_list_filesystem_assets,
dedupe_result_assets_payload=_dedupe_result_assets_payload,
+3 -14
View File
@@ -12,6 +12,7 @@ from pathlib import Path
from urllib.parse import unquote
from aiohttp import web
from mjr_am_backend.adapters.comfy_core import get_input_directory
from mjr_am_backend.config import get_runtime_output_root
from mjr_am_backend.custom_roots import list_custom_roots, resolve_custom_root
from mjr_am_backend.features.viewer.info import build_viewer_media_info
@@ -59,18 +60,6 @@ _ALLOWED_VIEWER_RESOURCE_EXTS = {
".spz",
}
try:
import folder_paths # type: ignore
except Exception:
class _FolderPathsStub:
@staticmethod
def get_input_directory() -> str:
return str((Path(__file__).resolve().parents[3] / "input").resolve())
folder_paths = _FolderPathsStub() # type: ignore
def _runtime_output_root() -> Path | None:
try:
return Path(get_runtime_output_root()).resolve(strict=False)
@@ -80,7 +69,7 @@ def _runtime_output_root() -> Path | None:
def _input_root() -> Path | None:
try:
return Path(folder_paths.get_input_directory()).resolve(strict=False)
return Path(str(get_input_directory() or "")).resolve(strict=False)
except Exception:
return None
@@ -322,7 +311,7 @@ def _resolve_base_root(root_id: str, asset_type: str) -> tuple[Path | None, Resu
if asset_type == "input":
try:
return Path(folder_paths.get_input_directory()).resolve(strict=False), None
return Path(str(get_input_directory() or "")).resolve(strict=False), None
except Exception:
return None, Result.Err("VIEW_FAILED", "Input directory is unavailable")
@@ -84,6 +84,9 @@ async def query_browser_rows(db: Any, filepaths: list[str]) -> list[dict] | None
SELECT
a.id,
a.filepath,
a.job_id,
a.source_node_id,
a.source_node_type,
COALESCE(m.rating, 0) AS rating,
COALESCE(m.tags, '[]') AS tags,
m.has_workflow AS has_workflow,
@@ -196,6 +199,13 @@ def hydrate_asset_from_row(asset: dict, by_fp: dict[str, dict]) -> None:
rid = row.get("id")
if rid is not None:
asset["id"] = int(rid)
for key in ("job_id", "source_node_id", "source_node_type"):
value = str(row.get(key) or "").strip()
if value:
asset[key] = value
file_info = asset.get("file_info")
if isinstance(file_info, dict):
file_info[key] = value
asset["rating"] = int(row.get("rating") or 0)
asset["tags"] = coerce_browser_tags(row.get("tags"))
asset["has_workflow"] = row.get("has_workflow")
+13 -1
View File
@@ -60,11 +60,17 @@ def test_collect_hydration_paths_skips_folder_assets() -> None:
def test_apply_hydration_rows_sets_id_rating_tags() -> None:
assets = [{"filepath": "/a/x.png", "kind": "image"}, {"filepath": "/a/folder", "kind": "folder"}]
assets: list[dict] = [
{"filepath": "/a/x.png", "kind": "image", "file_info": {}},
{"filepath": "/a/folder", "kind": "folder"},
]
rows = [
{
"id": "7",
"filepath": "/a/x.png",
"job_id": "prompt-1",
"source_node_id": "7",
"source_node_type": "SaveImage",
"rating": 5,
"tags": '["tag1"]',
"positive_prompt": "studio portrait",
@@ -77,6 +83,12 @@ def test_apply_hydration_rows_sets_id_rating_tags() -> None:
assert assets[0]["tags"] == ["tag1"]
assert assets[0]["positive_prompt"] == "studio portrait"
assert assets[0]["generation_time_ms"] == 8421
assert assets[0]["job_id"] == "prompt-1"
assert assets[0]["source_node_id"] == "7"
assert assets[0]["source_node_type"] == "SaveImage"
assert assets[0]["file_info"]["job_id"] == "prompt-1"
assert assets[0]["file_info"]["source_node_id"] == "7"
assert assets[0]["file_info"]["source_node_type"] == "SaveImage"
assert "id" not in assets[1]