From f4a2fee49225447469609dabbc8dd1a864935792 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:39:03 +0200 Subject: [PATCH] fix(connector): resolve server render paths on the workstation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QC's Open Render used the server's outputDir verbatim. The server addresses renders from its own root (render.outputRoot, e.g. //SAN), which the render worker translates via its own pathMappings because it runs as a service account — the panel had no equivalent, so Open Render failed on any machine that could not resolve that root. Resolution now tries, in order: an optional Path Mappings setting, the server path as given, then re-basing on Export Root. The last one is why this usually needs no configuration at all: both sides lay renders out as {root}/{showId}/{episode}/{shot}/{version}, so the tail can simply be re-attached. A failure names every candidate it tried rather than reporting one path that was never going to work. Diagnostics gained the same check against a real folder from the QC queue, since "Export Root exists" and "this machine can reach a render the server wrote" are different questions and only the second one breaks QC. Verified against the live V: drive: a //SAN path, a backslash variant and an explicitly-mapped path all resolve to the real render folder, while an unknown root and a different show correctly resolve to nothing. Co-Authored-By: Claude Opus 5 --- VFXReviewConnector.jsx | 192 +++++++++++++++++++++++++++++++++++++++-- VFXReviewConnector.md | 22 +++++ 2 files changed, 205 insertions(+), 9 deletions(-) diff --git a/VFXReviewConnector.jsx b/VFXReviewConnector.jsx index 8ce7bdf..c208c58 100644 --- a/VFXReviewConnector.jsx +++ b/VFXReviewConnector.jsx @@ -12,7 +12,13 @@ var CONFIG = { bannerImagePath:"", artistEmail: "", handleFrames: 8, - lastProjectCode:"" + lastProjectCode:"", + // Optional prefix rewrites for paths the *server* supplies (render folders + // are addressed as //SAN/... there, which a workstation may not resolve). + // Format: "//SAN/=V:/_EXPORTS/" — several separated by ";". + // Usually unnecessary: see resolveServerPath, which derives the local path + // from Export Root when the layouts mirror each other. + pathMappings: "" }; var SETTINGS_GROUP = "VFXReviewConnector"; @@ -220,7 +226,8 @@ var PANEL_VERSION = "2.1.0"; " | footageRoot=" + (CONFIG.footageRoot || "(empty)") + " | exportRoot=" + (CONFIG.exportRoot || "(empty)") + " | artistEmail=" + (CONFIG.artistEmail || "(empty)") + - " | handleFrames=" + CONFIG.handleFrames; + " | handleFrames=" + CONFIG.handleFrames + + " | pathMappings=" + (CONFIG.pathMappings || "(none)"); } function diagStart(action) { @@ -786,6 +793,107 @@ var PANEL_VERSION = "2.1.0"; return path + "/" + shot.shotCode + "/" + (shot.shotVersion || "v001"); } + // ── Server paths → this machine ─────────────────────────────────────────── + // + // The server addresses render folders from its own root (render.outputRoot, + // e.g. "//SAN"), which a workstation may not resolve. The worker solves this + // with pathMappings because it runs as a service account; the panel resolves + // it here, and mostly without configuration: the server's export layout + // mirrors the panel's from the show folder down — + // {root}/{showId}/{episode}/{shotCode}/{version} on both sides — so the tail + // can simply be re-based onto Export Root. + + function normalizeSlashes(path) { + return String(path).replace(/\\/g, "/"); + } + + function parsePathMappings() { + var raw = trimText(CONFIG.pathMappings); + var mappings = []; + var parts; + var i; + var pair; + + if (!raw) { + return mappings; + } + + parts = raw.split(";"); + for (i = 0; i < parts.length; i += 1) { + pair = trimText(parts[i]).split("="); + if (pair.length === 2 && trimText(pair[0]) && trimText(pair[1])) { + mappings.push({ from: normalizeSlashes(trimText(pair[0])), to: normalizeSlashes(trimText(pair[1])) }); + } + } + return mappings; + } + + function applyPathMappings(path) { + var mappings = parsePathMappings(); + var normalized = normalizeSlashes(path); + var i; + + for (i = 0; i < mappings.length; i += 1) { + if (normalized.toLowerCase().indexOf(mappings[i].from.toLowerCase()) === 0) { + return mappings[i].to + normalized.substring(mappings[i].from.length); + } + } + return null; + } + + // Re-base a server path onto Export Root from the show folder down, which + // works whenever both sides use the documented layout. + function rebaseOnExportRoot(path) { + var showId = getShowId(); + var normalized = normalizeSlashes(path); + var marker; + var index; + + if (!showId || !trimText(CONFIG.exportRoot)) { + return null; + } + + marker = "/" + showId + "/"; + index = normalized.toLowerCase().indexOf(marker.toLowerCase()); + if (index < 0) { + return null; + } + + return normalizeSlashes(trimText(CONFIG.exportRoot).replace(/\/+$/, "")) + normalized.substring(index); + } + + /** + * Resolves a server-supplied path to something that exists on this machine. + * Returns { path, how, tried } — `path` is null when nothing resolved, and + * `tried` lists every candidate so a failure names them all. + */ + function resolveServerPath(serverPath) { + var tried = []; + var candidates = []; + var mapped = applyPathMappings(serverPath); + var rebased; + var i; + + if (mapped) { + candidates.push({ path: mapped, how: "path mapping" }); + } + candidates.push({ path: normalizeSlashes(serverPath), how: "server path" }); + + rebased = rebaseOnExportRoot(serverPath); + if (rebased) { + candidates.push({ path: rebased, how: "re-based on Export Root" }); + } + + for (i = 0; i < candidates.length; i += 1) { + tried.push(candidates[i].how + ": " + candidates[i].path); + if (new Folder(candidates[i].path).exists) { + return { path: candidates[i].path, how: candidates[i].how, tried: tried }; + } + } + + return { path: null, how: "", tried: tried }; + } + function ensureFolder(path) { var folder = new Folder(path); var parent; @@ -1793,6 +1901,46 @@ var PANEL_VERSION = "2.1.0"; "\n" + failure.detail, diagReport()); } + /** + * Takes a real render folder from the server and checks this machine can + * reach it. The server addresses renders from its own root, so a + * workstation that has Export Root mapped can still fail here — which is + * exactly what QC's Open Render trips over. Returns a problem count. + */ + function checkServerPathReachable() { + var url = CONFIG.baseUrl + "/api/ext/qc/queue" + + (CURRENT_PROJECT ? "?projectCode=" + encodeURIComponent(CURRENT_PROJECT.code) : ""); + var data = getAPIData(url); + var entries; + var resolved; + var i; + + if (!data) { + diagLog("QC queue not reachable — skipping the render-path check"); + return 0; + } + + entries = getListFromResponse(data, "queue"); + if (!entries.length) { + diagLog("no exports awaiting QC — render-path check skipped (nothing to point at)"); + return 0; + } + + resolved = resolveServerPath(entries[0].outputDir); + if (resolved.path) { + diagLog("render folder OK: " + entries[0].shotCode + " via " + resolved.how + " -> " + resolved.path); + return 0; + } + + diagLog("PROBLEM cannot reach the render folder for " + entries[0].shotCode + + " — QC's Open Render will fail on this machine"); + for (i = 0; i < resolved.tried.length; i += 1) { + diagLog(" tried " + resolved.tried[i]); + } + diagLog(" - fix: set Export Root to match the layout, or add Settings -> Path Mappings, e.g. //SAN/=V:/_EXPORTS/"); + return 1; + } + // Checks everything Build Shot depends on, without building anything, so a // workstation can be cleared start to finish before an artist is blocked. function runDiagnostics() { @@ -1909,6 +2057,11 @@ var PANEL_VERSION = "2.1.0"; (DIAG.lastResponse ? summariseResponse(DIAG.lastResponse) : "no response")); } } + + // Can this machine actually reach a render folder the server wrote? + // That is a different question from Export Root existing, and it is + // the one QC fails on. + problems += checkServerPathReachable(); } for (i = 0; i < requiredComps.length; i += 1) { @@ -2919,6 +3072,12 @@ var PANEL_VERSION = "2.1.0"; var fBanner = addField(dlg, "Banner Path:", CONFIG.bannerImagePath); var fEmail = addField(dlg, "Artist Email:", CONFIG.artistEmail); var fHandles= addField(dlg, "Handle Frames:", String(CONFIG.handleFrames)); + var fMappings = addField(dlg, "Path Mappings:", CONFIG.pathMappings); + fMappings.helpTip = + "Optional. Rewrites paths the server supplies (render folders are\n" + + "addressed from its own root). Format: //SAN/=V:/_EXPORTS/\n" + + "Several separated by ';'. Usually unnecessary — the panel first\n" + + "tries the path as given, then re-bases it on Export Root."; var btnRow = dlg.add("group"); btnRow.orientation = "row"; @@ -2939,6 +3098,7 @@ var PANEL_VERSION = "2.1.0"; CONFIG.bannerImagePath = trimText(fBanner.text); CONFIG.artistEmail = trimText(fEmail.text); CONFIG.handleFrames = parseInt(fHandles.text, 10) || 8; + CONFIG.pathMappings = trimText(fMappings.text); saveConfig(); populateProjectDropdown(); } @@ -3871,7 +4031,11 @@ var PANEL_VERSION = "2.1.0"; // The export's own outputPattern prefix, so a version folder that also holds // the preview MOV/MP4 still resolves to the EXR sequence. function importQCSequence(entry) { - var folder = new Folder(entry.outputDir); + // The server addresses this folder from its own render root, which this + // machine may not resolve — resolveServerPath tries the mapping, the + // literal path, then Export Root (§ Server paths → this machine). + var resolved = resolveServerPath(entry.outputDir); + var folder; var prefix = String(entry.outputPattern || "").split("[")[0].replace(/\.$/, ""); var files; var importOptions; @@ -3886,14 +4050,21 @@ var PANEL_VERSION = "2.1.0"; return true; } - if (!folder.exists) { - setQCStatus("Render folder not found: " + entry.outputDir); - diagFail("QC import", entry.outputDir + " does not exist on this machine", - [describeMissingPath(entry.outputDir), - "The render folder is written by the farm — check this machine maps the same drive."]); + if (!resolved.path) { + setQCStatus("Render folder not found for " + entry.shotCode + " — see the report"); + diagFail("QC import", "could not resolve " + entry.outputDir + " on this machine", + resolved.tried.concat([ + "The server writes renders under its own root (render.outputRoot); this machine has to reach the same storage.", + "Either set Export Root so the layout lines up, or add a rewrite in Settings → Path Mappings, e.g. //SAN/=V:/_EXPORTS/" + ])); return false; } + folder = new Folder(resolved.path); + if (resolved.how !== "server path") { + diagLog("QC import: resolved " + entry.outputDir + " via " + resolved.how + " -> " + resolved.path); + } + files = folder.getFiles(function (f) { return f instanceof File && /\.exr$/i.test(f.name) && (!prefix || f.name.indexOf(prefix) === 0); @@ -3902,7 +4073,10 @@ var PANEL_VERSION = "2.1.0"; files = folder.getFiles("*.exr"); } if (!files || files.length < 1) { - setQCStatus("No EXRs in " + entry.outputDir); + setQCStatus("No EXRs in " + resolved.path); + diagFail("QC import", "no .exr files in " + resolved.path, + ["Resolved via " + resolved.how + " from the server path " + entry.outputDir + ".", + "Folder contents: " + describeFolderContents(folder)]); return false; } diff --git a/VFXReviewConnector.md b/VFXReviewConnector.md index 1581824..8e2da80 100644 --- a/VFXReviewConnector.md +++ b/VFXReviewConnector.md @@ -17,6 +17,28 @@ Settings are stored per-machine via the **⚙ Settings** button in the panel (pe | **Banner Path** | Path to the panel banner image | | **Artist Email** | Used for render attribution in the pipeline | | **Handle Frames** | Plate handle frames prepended before the shot's source TC in-point (default: 8) | +| **Path Mappings** | *Optional.* Rewrites paths the **server** supplies — see below | + +### Server paths vs. this machine + +The server addresses render folders from its own root (`render.outputRoot`, +e.g. `//SAN`), which a workstation may not resolve — the render worker +translates them with its own `pathMappings` because it runs as a service +account. The panel needs the same translation to open a render for QC. + +It resolves this itself in most cases, trying in order: + +1. **Path Mappings**, if configured — format `//SAN/=V:/_EXPORTS/`, several + separated by `;`. +2. **The server path as given** — correct when `render.outputRoot` is already + a path this machine can reach. +3. **Re-based on Export Root** — the server's export layout mirrors the panel's + from the show folder down (`{root}/{showId}/{episode}/{shot}/{version}`), so + the tail is simply re-attached to Export Root. + +Step 3 is why this usually needs no configuration. Set Path Mappings only when +the two layouts genuinely differ. **Diagnostics** checks this against a real +render folder from the QC queue and names every path it tried when it fails. ---