76 lines
3.3 KiB
JavaScript
76 lines
3.3 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
function orderCurrentTargetLast(targets, currentSourcePath) {
|
|
if (!currentSourcePath) return [...targets];
|
|
const current = path.resolve(currentSourcePath);
|
|
return [...targets].sort((left, right) => {
|
|
const leftIsCurrent = path.resolve(left.sourcePath) === current;
|
|
const rightIsCurrent = path.resolve(right.sourcePath) === current;
|
|
return Number(leftIsCurrent) - Number(rightIsCurrent);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply one rendered frame to explicitly selected Eagle items.
|
|
* Each item gets an independent temporary file because Eagle may consume or
|
|
* defer reading the file passed to setCustomThumbnail.
|
|
*/
|
|
async function applyThumbnailTargets(options) {
|
|
const targets = options.targets || [];
|
|
const settleDelayMs = options.settleDelayMs === undefined ? 800 : options.settleDelayMs;
|
|
const cleanupDelayMs = options.cleanupDelayMs === undefined ? 1200 : options.cleanupDelayMs;
|
|
const retryAttempts = options.retryAttempts === undefined ? 3 : options.retryAttempts;
|
|
const delay = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
const tempPaths = [];
|
|
let success = 0;
|
|
let skipped = Number(options.initialSkipped) || 0;
|
|
let failed = 0;
|
|
for (let index = 0; index < targets.length; index++) {
|
|
if (options.onProgress) options.onProgress(index + 1, targets.length);
|
|
let itemTempPath = null;
|
|
try {
|
|
const target = targets[index];
|
|
const fresh = await options.getItem(target.sourcePath);
|
|
if (fresh.data.customThumbnail === true && target.allowOverwrite !== true) { skipped++; continue; }
|
|
itemTempPath = path.join(os.tmpdir(), 'spineviewer-group-' + fresh.id + '-' + crypto.randomBytes(12).toString('hex') + options.rendered.extension);
|
|
fs.writeFileSync(itemTempPath, options.rendered.buffer);
|
|
tempPaths.push(itemTempPath);
|
|
let applied = false;
|
|
let lastError = null;
|
|
for (let attempt = 0; attempt < retryAttempts && !applied; attempt++) {
|
|
try {
|
|
const accepted = await options.setThumbnail(fresh.id, itemTempPath);
|
|
if (accepted !== true) throw new Error(options.notConfirmedMessage || 'Thumbnail update was rejected');
|
|
applied = await options.waitForApplied(target.sourcePath);
|
|
if (!applied) throw new Error(options.notConfirmedMessage || 'Thumbnail update was not confirmed');
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt < retryAttempts - 1) await delay(settleDelayMs);
|
|
}
|
|
}
|
|
if (!applied) throw lastError || new Error(options.notConfirmedMessage || 'Thumbnail update was not confirmed');
|
|
// The local HTTP API can resolve before Eagle has finished consuming the
|
|
// supplied file. Keep every file alive and serialize requests with a short
|
|
// settling interval so a later request cannot replace an earlier queued job.
|
|
await delay(settleDelayMs);
|
|
success++;
|
|
if (options.onSuccess) options.onSuccess(fresh);
|
|
} catch (error) {
|
|
failed++;
|
|
if (options.onError) options.onError(error);
|
|
} finally { /* temporary inputs are retained until the whole queue settles */ }
|
|
}
|
|
await delay(cleanupDelayMs);
|
|
for (const tempPath of tempPaths) {
|
|
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) { /* ignore */ }
|
|
}
|
|
return { success, skipped, failed };
|
|
}
|
|
|
|
module.exports = { applyThumbnailTargets, orderCurrentTargetLast };
|