diff --git a/workflows/media-handoff-orchestrator.json b/workflows/media-handoff-orchestrator.json index cf8e15f..4e82533 100644 --- a/workflows/media-handoff-orchestrator.json +++ b/workflows/media-handoff-orchestrator.json @@ -240,7 +240,7 @@ }, { "parameters": { - "jsCode": "const state = $getWorkflowStaticData('global');\nstate.jobs = state.jobs || {};\nconst jobs = state.jobs;\nconst now = Date.now();\nconst norm = (v) => String(v ?? '').replace(/\\.nzb$/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '');\nconst flatten = (nodeName) => {\n const out = [];\n for (const item of $(nodeName).all()) {\n const value = item.json;\n if (Array.isArray(value)) out.push(...value);\n else if (Array.isArray(value?.records)) out.push(...value.records);\n else if (value?.eventType) out.push(value);\n }\n return out;\n};\nconst sources = {\n sonarr: flatten('History Sonarr UHD'),\n sonarr1080p: flatten('History Sonarr FHD'),\n radarr: flatten('History Radarr UHD'),\n radarr1080p: flatten('History Radarr FHD'),\n};\nconst imported = new Set(['downloadFolderImported']);\nconst failed = new Set(['downloadFailed', 'downloadIgnored']);\nconst changed = [];\nfor (const job of Object.values(jobs)) {\n if (!['registered', 'processing'].includes(job.state)) continue;\n const start = Date.parse(job.startedAt);\n const wanted = new Set([norm(job.release), norm(job.cleanName)].filter(Boolean));\n const events = (sources[job.service] || []).filter(e => {\n const at = Date.parse(e.date || 0);\n return at >= start && wanted.has(norm(e.sourceTitle));\n }).sort((a,b) => Date.parse(a.date) - Date.parse(b.date));\n const terminals = events.filter(e => imported.has(e.eventType) || failed.has(e.eventType));\n const latest = terminals.at(-1);\n const importEvents = events.filter(e => imported.has(e.eventType));\n const importedUnits = new Set(importEvents.map(e => String(\n job.kind === 'episode' ? (e.episodeId ?? e.data?.fileId ?? e.id) : (e.movieId ?? e.data?.fileId ?? e.id)\n )));\n job.importedFiles = importedUnits.size;\n const expectedFiles = Number(job.expectedFiles ?? 1);\n if (importedUnits.size >= expectedFiles) {\n const completed = importEvents.at(-1);\n job.state = 'success'; job.completedAt = completed.date; job.updatedAt = new Date(now).toISOString();\n job.importEventId = completed.id ?? null;\n changed.push({ jobId: job.jobId, state: job.state, importedFiles: importedUnits.size }); continue;\n }\n if (latest && failed.has(latest.eventType)) {\n job.state = 'failed'; job.failedAt = latest.date; job.updatedAt = new Date(now).toISOString();\n job.reason = latest.eventType; changed.push({ jobId: job.jobId, state: job.state }); continue;\n }\n const wasRegistered = job.state === 'registered';\n const maxAge = wasRegistered ? 15 * 60000 : 24 * 3600000;\n if (now - start > maxAge) {\n job.state = 'timeout'; job.timeoutAt = new Date(now).toISOString(); job.updatedAt = job.timeoutAt;\n job.reason = wasRegistered ? 'move_not_confirmed' : 'import_timeout';\n changed.push({ jobId: job.jobId, state: job.state });\n }\n}\nreturn [{ json: { ok: true, activeJobs: Object.values(jobs).filter(j => ['registered','processing'].includes(j.state)).length, changed } }];" + "jsCode": "const state = $getWorkflowStaticData('global');\nstate.jobs = state.jobs || {};\nconst jobs = state.jobs;\nconst now = Date.now();\nconst norm = (v) => String(v ?? '').replace(/\\.nzb$/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '');\nconst flatten = (nodeName) => {\n const out = [];\n for (const item of $(nodeName).all()) {\n const value = item.json;\n if (Array.isArray(value)) out.push(...value);\n else if (Array.isArray(value?.records)) out.push(...value.records);\n else if (value?.eventType) out.push(value);\n }\n return out;\n};\nconst sources = {\n sonarr: flatten('History Sonarr UHD'),\n sonarr1080p: flatten('History Sonarr FHD'),\n radarr: flatten('History Radarr UHD'),\n radarr1080p: flatten('History Radarr FHD'),\n};\nconst imported = new Set(['downloadFolderImported']);\nconst failed = new Set(['downloadFailed', 'downloadIgnored']);\nconst changed = [];\nfor (const job of Object.values(jobs)) {\n if (!['registered', 'processing'].includes(job.state)) continue;\n const start = Date.parse(job.startedAt);\n const wanted = new Set([norm(job.release), norm(job.cleanName)].filter(Boolean));\n const eventNames = (e) => {\n const names = [e.sourceTitle];\n const droppedPath = String(e.data?.droppedPath ?? '').replace(/\\\\/g, '/').replace(/\\/+$/, '');\n if (droppedPath) {\n const parts = droppedPath.split('/').filter(Boolean);\n names.push(parts.at(-1));\n if (parts.length > 1) names.push(parts.at(-2));\n }\n return names.map(norm).filter(Boolean);\n };\n const events = (sources[job.service] || []).filter(e => {\n const at = Date.parse(e.date || 0);\n return at >= start && eventNames(e).some(name => wanted.has(name));\n }).sort((a,b) => Date.parse(a.date) - Date.parse(b.date));\n const terminals = events.filter(e => imported.has(e.eventType) || failed.has(e.eventType));\n const latest = terminals.at(-1);\n const importEvents = events.filter(e => imported.has(e.eventType));\n const importedUnits = new Set(importEvents.map(e => String(\n job.kind === 'episode' ? (e.episodeId ?? e.data?.fileId ?? e.id) : (e.movieId ?? e.data?.fileId ?? e.id)\n )));\n job.importedFiles = importedUnits.size;\n const expectedFiles = Number(job.expectedFiles ?? 1);\n if (importedUnits.size >= expectedFiles) {\n const completed = importEvents.at(-1);\n job.state = 'success'; job.completedAt = completed.date; job.updatedAt = new Date(now).toISOString();\n job.importEventId = completed.id ?? null;\n changed.push({ jobId: job.jobId, state: job.state, importedFiles: importedUnits.size }); continue;\n }\n if (latest && failed.has(latest.eventType)) {\n job.state = 'failed'; job.failedAt = latest.date; job.updatedAt = new Date(now).toISOString();\n job.reason = latest.eventType; changed.push({ jobId: job.jobId, state: job.state }); continue;\n }\n const wasRegistered = job.state === 'registered';\n const maxAge = wasRegistered ? 15 * 60000 : 24 * 3600000;\n if (now - start > maxAge) {\n job.state = 'timeout'; job.timeoutAt = new Date(now).toISOString(); job.updatedAt = job.timeoutAt;\n job.reason = wasRegistered ? 'move_not_confirmed' : 'import_timeout';\n changed.push({ jobId: job.jobId, state: job.state });\n }\n}\nreturn [{ json: { ok: true, activeJobs: Object.values(jobs).filter(j => ['registered','processing'].includes(j.state)).length, changed } }];" }, "id": "54c60ec7-6854-4916-a4e3-9a2b7d13b503", "name": "Reconcile Jobs", @@ -372,4 +372,4 @@ "executionOrder": "v1", "errorWorkflow": "AOZA96TZqhB2Dici" } -} \ No newline at end of file +}