| ... | ... | @@ -14,30 +14,39 @@ const sotToken = process.env.CLOVER_SOT_KEY; |
| 14 | 14 | |
| 15 | 15 | export async function main() { |
| 16 | 16 | const start = performance.now(); |
| 17 | | const timerSpinner = new Spinner({ |
| 18 | | text: () => |
| 19 | | `paper clover's scan3 [${ |
| 20 | | ((performance.now() - start) / 1000).toFixed( |
| 21 | | 1, |
| 22 | | ) |
| 23 | | }s]`, |
| 24 | | fps: 10, |
| 17 | using _ = term.startWidget({ |
| 18 | format: (now) => |
| 19 | `paper clover's scan3 [${((now - start) / 1000).toFixed(1)}s]`, |
| 25 | 20 | }); |
| 26 | | using _endTimerSpinner = { [Symbol.dispose]: () => timerSpinner.stop() }; |
| 21 | |
| 22 | const walkQueue = new queue.PriorityQueue(10); |
| 23 | |
| 24 | const dirsNode = progress.start("Walk Tree", { estimate: 1 }); |
| 25 | const fileNode = progress.start("Process File", { estimate: 0 }); |
| 26 | fileNode.sortChildren = (a, b) => { |
| 27 | const ac = a.children.length > 0 ? 1 : 0; |
| 28 | const bc = b.children.length > 0 ? 1 : 0; |
| 29 | if (ac !== bc) return bc - ac; |
| 30 | return a.text.localeCompare(b.text); |
| 31 | }; |
| 27 | 32 | |
| 28 | 33 | // Read a directory or file stat and queue up changed files. |
| 29 | | using qList = new async.Queue({ |
| 30 | | name: "Discover Tree", |
| 31 | | async fn(absPath: string) { |
| 32 | | const stat = await fs.stat(absPath); |
| 34 | const scanDirectory = walkQueue.wrap( |
| 35 | async (path: Path) => { |
| 36 | const publicPath = toPublicPath(path); |
| 37 | using node = dirsNode.start(publicPath + " - stat"); |
| 38 | using _ = ts.defer(() => dirsNode.inc()); |
| 39 | |
| 40 | const stat = await path.stat(); |
| 33 | 41 | |
| 34 | | const publicPath = toPublicPath(Path.resolve(absPath)); |
| 35 | 42 | const mediaFile = MediaFile.getByPath(publicPath); |
| 36 | 43 | |
| 37 | 44 | if (stat.isDirectory()) { |
| 38 | | const items = (await fs.readdir(absPath)) |
| 39 | | .filter((basename) => !skipBasename(basename)); |
| 40 | | qList.addMany(items.map((subPath) => path.join(absPath, subPath))); |
| 45 | node.text = publicPath + " - reading"; |
| 46 | const items = (await path.readDir()) |
| 47 | .filter((child) => !skipBasename(child.base)) |
| 48 | .map((child) => (scanDirectory(child), child.base)); |
| 49 | dirsNode.estimate += items.length; |
| 41 | 50 | |
| 42 | 51 | if (mediaFile) { |
| 43 | 52 | const deleted = mediaFile |
| ... | ... | @@ -48,45 +57,39 @@ export async function main() { |
| 48 | 57 | ? child.getRecursiveFileChildren() |
| 49 | 58 | : child |
| 50 | 59 | ); |
| 51 | | |
| 52 | | qMeta.addMany( |
| 53 | | deleted.map((mediaFile) => ({ |
| 54 | | path: Path.resolve(root, mediaFile.path), |
| 55 | | publicPath: mediaFile.path, |
| 56 | | stat: null, |
| 57 | | mediaFile, |
| 58 | | })), |
| 59 | | ); |
| 60 | //deleted |
| 60 | 61 | } |
| 61 | 62 | |
| 62 | 63 | return; |
| 63 | 64 | } |
| 64 | 65 | |
| 65 | | // All processes must be performed again if there is no file. |
| 66 | 66 | if ( |
| 67 | | !mediaFile || |
| 67 | !mediaFile || // All processes must be performed if there is no file. |
| 68 | // Rerun all processors if it changed |
| 68 | 69 | stat.size !== mediaFile.size || |
| 69 | 70 | stat.mtime.getTime() !== mediaFile.date.getTime() |
| 70 | 71 | ) { |
| 71 | | qMeta.add({ path: new Path(absPath), publicPath, stat, mediaFile }); |
| 72 | | return; |
| 72 | updateMetadata({ path, publicPath, stat, mediaFile }); |
| 73 | } else { |
| 74 | // If the scanners changed, it may mean more processes should be run. |
| 75 | queueProcessors({ |
| 76 | path, |
| 77 | stat, |
| 78 | mediaFile, |
| 79 | node: fileNode.start(publicPath.slice(1)), |
| 80 | }); |
| 73 | 81 | } |
| 74 | | |
| 75 | | // If the scanners changed, it may mean more processes should be run. |
| 76 | | queueProcessors({ path: new Path(absPath), stat, mediaFile }); |
| 77 | 82 | }, |
| 78 | | maxJobs: 24, |
| 79 | | }); |
| 80 | | using qMeta = new async.Queue({ |
| 81 | | name: "Update Metadata", |
| 82 | | async fn({ path, publicPath, stat, mediaFile }: UpdateMetadataJob) { |
| 83 | | if (!stat) { |
| 84 | | // File was deleted. |
| 85 | | await runUndoProcessors(UNWRAP(mediaFile)); |
| 86 | | return; |
| 87 | | } |
| 88 | | await scrubLocationMetadata(path, stat); |
| 83 | ); |
| 84 | const updateMetadata = queue.wrap( |
| 85 | async ({ path, publicPath, stat, mediaFile }: UpdateMetadataArgs) => { |
| 86 | using errorHandler = new DisposableStack(); |
| 87 | const label = publicPath.slice(1); |
| 88 | const node = errorHandler.use(fileNode.start(label)); |
| 89 | 89 | |
| 90 | await scrubLocationMetadata(path, stat, node); |
| 91 | |
| 92 | node.text = `${label} - hashing`; |
| 90 | 93 | const hash = await new Promise<string>((resolve, reject) => { |
| 91 | 94 | const reader = fs.createReadStream(path.toString()); |
| 92 | 95 | reader.on("error", reject); |
| ... | ... | @@ -121,42 +124,46 @@ export async function main() { |
| 121 | 124 | dimensions: mediaFile?.dimensions ?? "", |
| 122 | 125 | contents: mediaFile?.contents ?? "", |
| 123 | 126 | }); |
| 127 | |
| 128 | node.text = `${label}`; |
| 124 | 129 | await queueProcessors({ |
| 125 | 130 | path, |
| 126 | 131 | stat, |
| 127 | 132 | mediaFile, |
| 133 | node, |
| 128 | 134 | }); |
| 135 | errorHandler.move(); |
| 129 | 136 | }, |
| 130 | | getItemText: (job) => |
| 131 | | job.publicPath.slice(1) + (job.stat ? "" : " (deleted)"), |
| 132 | | maxJobs: 10, |
| 133 | | }); |
| 134 | | using qProcess = new async.Queue({ |
| 135 | | name: "Process Contents", |
| 136 | | async fn( |
| 137 | | { path, stat, mediaFile, processor, index, after }: ProcessJob, |
| 138 | | spin, |
| 139 | | ) { |
| 140 | | await processor.run({ |
| 141 | | path, |
| 142 | | stat, |
| 143 | | mediaFile, |
| 144 | | spin, |
| 145 | | }); |
| 146 | | mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index))); |
| 147 | | for (const dependantJob of after) { |
| 148 | | ASSERT( |
| 149 | | dependantJob.needs > 0, |
| 150 | | `dependantJob.needs > 0, ${dependantJob.needs}`, |
| 151 | | ); |
| 152 | | dependantJob.needs -= 1; |
| 153 | | if (dependantJob.needs == 0) qProcess.add(dependantJob); |
| 137 | () => ({ priority: -1 }), |
| 138 | ); |
| 139 | const queueFileProcessor = queue.wrap(async function ({ |
| 140 | path, |
| 141 | stat, |
| 142 | mediaFile, |
| 143 | processor, |
| 144 | index, |
| 145 | after, |
| 146 | fileNode, |
| 147 | }: ProcessJob) { |
| 148 | using node = fileNode.start(processor.name); |
| 149 | await processor.run({ |
| 150 | path, |
| 151 | stat, |
| 152 | mediaFile, |
| 153 | node, |
| 154 | }); |
| 155 | mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index))); |
| 156 | for (const dependantJob of after) { |
| 157 | ASSERT( |
| 158 | dependantJob.needs > 0, |
| 159 | `dependantJob.needs > 0, ${dependantJob.needs}`, |
| 160 | ); |
| 161 | dependantJob.needs -= 1; |
| 162 | if (dependantJob.needs == 0) { |
| 163 | queueFileProcessor(dependantJob); |
| 154 | 164 | } |
| 155 | | }, |
| 156 | | getItemText: ({ mediaFile, processor }) => |
| 157 | | `${mediaFile.path.slice(1)} - ${processor.name}`, |
| 158 | | maxJobs: 4, |
| 159 | | }); |
| 165 | } |
| 166 | }, (job) => ({ cores: job.processor.cores })); |
| 160 | 167 | |
| 161 | 168 | function decodeProcessors(input: string) { |
| 162 | 169 | return input |
| ... | ... | @@ -172,7 +179,14 @@ export async function main() { |
| 172 | 179 | path, |
| 173 | 180 | stat, |
| 174 | 181 | mediaFile, |
| 182 | node, |
| 175 | 183 | }: Omit<ProcessFileArgs, "spin">) { |
| 184 | using errorHandler = new DisposableStack(); |
| 185 | errorHandler.use(node); |
| 186 | |
| 187 | node.showEstimate = false; |
| 188 | node.passive = true; |
| 189 | |
| 176 | 190 | const ext = mediaFile.extensionNonEmpty.toLowerCase(); |
| 177 | 191 | let possible = processors.filter((p) => |
| 178 | 192 | p.include ? p.include.has(ext) : !p.exclude?.has(ext) |
| ... | ... | @@ -196,11 +210,7 @@ export async function main() { |
| 196 | 210 | const p = processors.find((p) => p.id === id); |
| 197 | 211 | if (!p) continue; |
| 198 | 212 | const index = possible.indexOf(p); |
| 199 | | if (index !== -1 && p.hash === hash) { |
| 200 | | processed |= 1 << (16 + index); |
| 201 | | } else { |
| 202 | | if (p.undo) await p.undo(mediaFile); |
| 203 | | } |
| 213 | if (index !== -1 && p.hash === hash) processed |= 1 << (16 + index); |
| 204 | 214 | } |
| 205 | 215 | mediaFile.setProcessors( |
| 206 | 216 | processed, |
| ... | ... | @@ -218,19 +228,22 @@ export async function main() { |
| 218 | 228 | const jobs: ProcessJob[] = []; |
| 219 | 229 | for (let i = 0, { length } = possible; i < length; i += 1) { |
| 220 | 230 | if ((processed & (1 << (16 + i))) === 0) { |
| 231 | const processor = UNWRAP(possible[i]); |
| 221 | 232 | const job: ProcessJob = { |
| 222 | 233 | path, |
| 223 | 234 | stat, |
| 224 | 235 | mediaFile, |
| 225 | | processor: UNWRAP(possible[i]), |
| 236 | processor, |
| 226 | 237 | index: i, |
| 227 | 238 | after: [], |
| 228 | | needs: UNWRAP(possible[i]).depends.length, |
| 239 | needs: processor.depends.length, |
| 240 | fileNode: node, |
| 229 | 241 | }; |
| 230 | 242 | jobs.push(job); |
| 231 | | if (job.needs === 0) qProcess.add(job); |
| 243 | if (job.needs === 0) queueFileProcessor(job); |
| 232 | 244 | } |
| 233 | 245 | } |
| 246 | node.estimate = jobs.length; |
| 234 | 247 | for (const job of jobs) { |
| 235 | 248 | for (const dependId of job.processor.depends) { |
| 236 | 249 | const dependJob = jobs.find((j) => j.processor.id === dependId); |
| ... | ... | @@ -239,32 +252,19 @@ export async function main() { |
| 239 | 252 | } else { |
| 240 | 253 | ASSERT(job.needs > 0, `job.needs !== 0, ${job.needs}`); |
| 241 | 254 | job.needs -= 1; |
| 242 | | if (job.needs === 0) qProcess.add(job); |
| 255 | if (job.needs === 0) queueFileProcessor(job); |
| 243 | 256 | } |
| 244 | 257 | } |
| 245 | 258 | } |
| 246 | | } |
| 247 | | |
| 248 | | async function runUndoProcessors(mediaFile: MediaFile) { |
| 249 | | const { processed } = mediaFile; |
| 250 | | const previous = decodeProcessors(mediaFile.processors).filter( |
| 251 | | (_, i) => (processed & (1 << (16 + i))) !== 0, |
| 252 | | ); |
| 253 | | for (const { id } of previous) { |
| 254 | | const p = processors.find((p) => p.id === id); |
| 255 | | if (!p) continue; |
| 256 | | if (p.undo) { |
| 257 | | await p.undo(mediaFile); |
| 258 | | } |
| 259 | | } |
| 260 | | mediaFile.delete(); |
| 259 | if (node.estimate > 0) errorHandler.move(); |
| 261 | 260 | } |
| 262 | 261 | |
| 263 | 262 | // Add the root & recursively iterate! |
| 264 | | qList.add(root); |
| 265 | | await qList.done(); |
| 266 | | await qMeta.done(); |
| 267 | | await qProcess.done(); |
| 263 | scanDirectory(Path.resolve(root)); |
| 264 | // await qList.done(); |
| 265 | // await qMeta.done(); |
| 266 | // await qProcess.done(); |
| 267 | return; |
| 268 | 268 | |
| 269 | 269 | // Update directory metadata |
| 270 | 270 | const dirs = MediaFile.getDirectoriesToReindex() |
| ... | ... | @@ -401,6 +401,7 @@ export async function main() { |
| 401 | 401 | |
| 402 | 402 | interface Process { |
| 403 | 403 | name: string; |
| 404 | cores: number; |
| 404 | 405 | enable?: boolean; |
| 405 | 406 | include?: Set<string>; |
| 406 | 407 | exclude?: Set<string>; |
| ... | ... | @@ -408,8 +409,6 @@ interface Process { |
| 408 | 409 | version?: number; |
| 409 | 410 | /* Perform an action. */ |
| 410 | 411 | run(args: ProcessFileArgs): Promise<void>; |
| 411 | | /* Should detect if `run` was never even run before before undoing state */ |
| 412 | | undo?(mediaFile: MediaFile): Promise<void>; |
| 413 | 412 | } |
| 414 | 413 | |
| 415 | 414 | const ffprobeBin = testProgram("ffprobe", "--help"); |
| ... | ... | @@ -422,6 +421,7 @@ const procDuration: Process = { |
| 422 | 421 | name: "calculate duration", |
| 423 | 422 | enable: ffprobeBin !== null, |
| 424 | 423 | include: rules.extsDuration, |
| 424 | cores: 1, |
| 425 | 425 | async run({ path, mediaFile }) { |
| 426 | 426 | const { stdout } = await subprocess.exec(ffprobeBin!, [ |
| 427 | 427 | "-v", |
| ... | ... | @@ -445,6 +445,7 @@ const procDimensions: Process = { |
| 445 | 445 | name: "calculate dimensions", |
| 446 | 446 | enable: ffprobeBin != null, |
| 447 | 447 | include: rules.extsDimensions, |
| 448 | cores: 1, |
| 448 | 449 | async run({ path, mediaFile }) { |
| 449 | 450 | const { ext } = path; |
| 450 | 451 | |
| ... | ... | @@ -484,6 +485,7 @@ const procDimensions: Process = { |
| 484 | 485 | const procLoadTextContents: Process = { |
| 485 | 486 | name: "load text content", |
| 486 | 487 | include: rules.extsReadContents, |
| 488 | cores: 1, |
| 487 | 489 | async run({ path, mediaFile, stat }) { |
| 488 | 490 | if (stat.size > 1_000_000) return; |
| 489 | 491 | const text = await path.read("utf-8"); |
| ... | ... | @@ -494,6 +496,7 @@ const procLoadTextContents: Process = { |
| 494 | 496 | const procHighlightCode: Process = { |
| 495 | 497 | name: "highlight source code", |
| 496 | 498 | include: new Set(rules.extsCode.keys()), |
| 499 | cores: 1, |
| 497 | 500 | async run({ path, mediaFile, stat }) { |
| 498 | 501 | const language = UNWRAP( |
| 499 | 502 | rules.extsCode.get(path.ext.toLowerCase()), |
| ... | ... | @@ -523,16 +526,17 @@ const procImageSubsets: Process = { |
| 523 | 526 | include: rules.extsImage, |
| 524 | 527 | depends: [procDimensions.name], |
| 525 | 528 | version: 2, |
| 526 | | async run({ path, mediaFile, spin }) { |
| 529 | cores: 2, |
| 530 | async run({ path, mediaFile, node }) { |
| 527 | 531 | const { width, height } = UNWRAP(mediaFile.parseDimensions()); |
| 528 | 532 | const targetSizes = transcodeRules.imageSizes.filter((w) => w < width); |
| 529 | | const baseStatus = spin.text; |
| 533 | const baseStatus = node.text; |
| 530 | 534 | |
| 531 | 535 | using stack = new DisposableStack(); |
| 532 | 536 | for (const size of targetSizes) { |
| 533 | 537 | const { w, h } = resizeDimensions(width, height, size); |
| 534 | 538 | for (const { ext, args } of transcodeRules.imagePresets) { |
| 535 | | spin.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`; |
| 539 | node.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`; |
| 536 | 540 | |
| 537 | 541 | stack.use( |
| 538 | 542 | await derived.produce(mediaFile, `${size}${ext}`, async (dir) => { |
| ... | ... | @@ -570,22 +574,16 @@ const procVideos = transcodeRules.videoFormats.map<Process>((preset) => ({ |
| 570 | 574 | include: rules.extsVideo, |
| 571 | 575 | enable: ffmpegBin != null, |
| 572 | 576 | depends: [procDuration.name, procDimensions.name], |
| 573 | | async run({ path, mediaFile, spin }) { |
| 577 | cores: 4, |
| 578 | async run({ path, mediaFile, node }) { |
| 574 | 579 | if ((mediaFile.duration ?? 0) < 5) return; |
| 575 | 580 | await derived.produce(mediaFile, `av1-${preset.id}`, async (dir) => { |
| 576 | 581 | const input = await videoInputArgsCache.getOrRun(path); |
| 577 | 582 | const args = transcodeRules.getAv1VideoArgs(preset, input.video, dir); |
| 578 | 583 | |
| 579 | | const fakeProgress = new Progress({ text: spin.text, spinner: null }); |
| 580 | | fakeProgress.stop(); |
| 581 | | spin.format = (now: number) => fakeProgress.format(now); |
| 582 | | // @ts-expect-error |
| 583 | | fakeProgress.redraw = () => spin.redraw(); |
| 584 | | |
| 585 | 584 | await ffmpeg.spawn({ |
| 586 | 585 | ffmpeg: ffmpegBin!, |
| 587 | | title: fakeProgress.text, |
| 588 | | progress: fakeProgress, |
| 586 | progress: node, |
| 589 | 587 | args, |
| 590 | 588 | cwd: dir, |
| 591 | 589 | }); |
| ... | ... | @@ -597,23 +595,17 @@ const procVideoAudios = transcodeRules.audioFormats.map<Process>((preset) => ({ |
| 597 | 595 | include: rules.extsVideo, |
| 598 | 596 | enable: ffmpegBin != null, |
| 599 | 597 | depends: [procDuration.name, procDimensions.name], |
| 600 | | async run({ path, mediaFile, spin }) { |
| 598 | cores: 2, |
| 599 | async run({ path, mediaFile, node }) { |
| 601 | 600 | if ((mediaFile.duration ?? 0) < 5) return; |
| 602 | 601 | await derived.produce(mediaFile, `opus-${preset.id}`, async (dir) => { |
| 603 | 602 | const input = await videoInputArgsCache.getOrRun(path); |
| 604 | 603 | if (!input.audio) return; |
| 605 | 604 | const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir); |
| 606 | 605 | |
| 607 | | const fakeProgress = new Progress({ text: spin.text, spinner: null }); |
| 608 | | fakeProgress.stop(); |
| 609 | | spin.format = (now: number) => fakeProgress.format(now); |
| 610 | | // @ts-expect-error |
| 611 | | fakeProgress.redraw = () => spin.redraw(); |
| 612 | | |
| 613 | 606 | await ffmpeg.spawn({ |
| 614 | 607 | ffmpeg: ffmpegBin!, |
| 615 | | title: fakeProgress.text, |
| 616 | | progress: fakeProgress, |
| 608 | progress: node, |
| 617 | 609 | args, |
| 618 | 610 | cwd: dir, |
| 619 | 611 | }); |
| ... | ... | @@ -625,7 +617,8 @@ const procDash: Process = { |
| 625 | 617 | include: rules.extsVideo, |
| 626 | 618 | enable: ffmpegBin != null, |
| 627 | 619 | depends: [...procVideos, ...procVideoAudios].map((x) => x.name), |
| 628 | | async run({ path, mediaFile, spin }) { |
| 620 | cores: 1, |
| 621 | async run({ path, mediaFile, node }) { |
| 629 | 622 | if ((mediaFile.duration ?? 0) < 5) return; |
| 630 | 623 | await derived.produce(mediaFile, `dash-av1`, async (dir) => { |
| 631 | 624 | const input = await videoInputArgsCache.getOrRun(path); |
| ... | ... | @@ -649,16 +642,9 @@ const procDash: Process = { |
| 649 | 642 | |
| 650 | 643 | await dir.join("d").makeDir(); |
| 651 | 644 | |
| 652 | | const fakeProgress = new Progress({ text: spin.text, spinner: null }); |
| 653 | | fakeProgress.stop(); |
| 654 | | spin.format = (now: number) => fakeProgress.format(now); |
| 655 | | // @ts-expect-error |
| 656 | | fakeProgress.redraw = () => spin.redraw(); |
| 657 | | |
| 658 | 645 | await ffmpeg.spawn({ |
| 659 | 646 | ffmpeg: ffmpegBin!, |
| 660 | | title: fakeProgress.text, |
| 661 | | progress: fakeProgress, |
| 647 | progress: node, |
| 662 | 648 | args, |
| 663 | 649 | cwd: dir, |
| 664 | 650 | }); |
| ... | ... | @@ -670,22 +656,16 @@ const procH264Hls: Process = { |
| 670 | 656 | include: rules.extsVideo, |
| 671 | 657 | enable: ffmpegBin != null, |
| 672 | 658 | depends: [procDuration.name, procDimensions.name], |
| 673 | | async run({ path, mediaFile, spin }) { |
| 659 | cores: 8, |
| 660 | async run({ path, mediaFile, node }) { |
| 674 | 661 | if ((mediaFile.duration ?? 0) < 5) return; |
| 675 | 662 | await derived.produce(mediaFile, `hls`, async (dir) => { |
| 676 | 663 | const input = await videoInputArgsCache.getOrRun(path); |
| 677 | 664 | const args = transcodeRules.getH264HlsArgs(input, dir); |
| 678 | 665 | |
| 679 | | const fakeProgress = new Progress({ text: spin.text, spinner: null }); |
| 680 | | fakeProgress.stop(); |
| 681 | | spin.format = (now: number) => fakeProgress.format(now); |
| 682 | | // @ts-expect-error |
| 683 | | fakeProgress.redraw = () => spin.redraw(); |
| 684 | | |
| 685 | 666 | await ffmpeg.spawn({ |
| 686 | 667 | ffmpeg: ffmpegBin!, |
| 687 | | title: fakeProgress.text, |
| 688 | | progress: fakeProgress, |
| 668 | progress: node, |
| 689 | 669 | args, |
| 690 | 670 | cwd: dir, |
| 691 | 671 | }); |
| ... | ... | @@ -700,6 +680,7 @@ const procCompression = [ |
| 700 | 680 | ({ name, fn }) => ({ |
| 701 | 681 | name: `compress ${name}`, |
| 702 | 682 | exclude: rules.extsPreCompressed, |
| 683 | cores: 1, |
| 703 | 684 | async run({ path, mediaFile }) { |
| 704 | 685 | await derived.produce(mediaFile, name, async (dir) => { |
| 705 | 686 | await stream.promises.pipeline( |
| ... | ... | @@ -752,10 +733,10 @@ function resizeDimensions(w: number, h: number, desiredWidth: number) { |
| 752 | 733 | return { w: desiredWidth, h: Math.floor((h / w) * desiredWidth) }; |
| 753 | 734 | } |
| 754 | 735 | |
| 755 | | interface UpdateMetadataJob { |
| 736 | interface UpdateMetadataArgs { |
| 756 | 737 | path: Path; |
| 757 | 738 | publicPath: string; |
| 758 | | stat: fs.Stats | null; |
| 739 | stat: fs.Stats; |
| 759 | 740 | mediaFile: MediaFile | null; |
| 760 | 741 | } |
| 761 | 742 | |
| ... | ... | @@ -763,7 +744,7 @@ interface ProcessFileArgs { |
| 763 | 744 | path: Path; |
| 764 | 745 | stat: fs.Stats; |
| 765 | 746 | mediaFile: MediaFile; |
| 766 | | spin: Spinner; |
| 747 | node: progress.Node; |
| 767 | 748 | } |
| 768 | 749 | |
| 769 | 750 | interface ProcessJob { |
| ... | ... | @@ -774,6 +755,7 @@ interface ProcessJob { |
| 774 | 755 | index: number; |
| 775 | 756 | after: ProcessJob[]; |
| 776 | 757 | needs: number; |
| 758 | fileNode: progress.Node; |
| 777 | 759 | } |
| 778 | 760 | |
| 779 | 761 | function skipBasename(basename: string): boolean { |
| ... | ... | @@ -810,7 +792,9 @@ function testProgram(name: string, helpArgument: string) { |
| 810 | 792 | async function scrubLocationMetadata( |
| 811 | 793 | path: Path, |
| 812 | 794 | stats: fs.Stats, |
| 795 | progress: progress.Ref, |
| 813 | 796 | ): Promise<boolean> { |
| 797 | using _ = progress.start("scrub exif metadata"); |
| 814 | 798 | const ext = path.ext.toLowerCase(); |
| 815 | 799 | if (!rules.extsScrubExif.has(ext)) return false; |
| 816 | 800 | |
| ... | ... | @@ -894,7 +878,7 @@ async function scrubLocationMetadata( |
| 894 | 878 | } |
| 895 | 879 | |
| 896 | 880 | // Restore original timestamps |
| 897 | | await fsp.rename(tempOutput.toString(), path); |
| 881 | await fsp.rename(tempOutput.toString(), path.toString()); |
| 898 | 882 | await fsp.utimes(path.toString(), accessTime, modTime); |
| 899 | 883 | |
| 900 | 884 | // Backup is no longer needed |
| ... | ... | @@ -920,12 +904,14 @@ async function scrubLocationMetadata( |
| 920 | 904 | |
| 921 | 905 | const monthMilliseconds = 30 * 24 * 60 * 60 * 1000; |
| 922 | 906 | |
| 923 | | import { Progress } from "@paperclover/console/Progress"; |
| 924 | | import { Spinner } from "@paperclover/console/Spinner"; |
| 925 | 907 | import * as async from "#sitegen/async"; |
| 926 | 908 | import * as fs from "#sitegen/fs"; |
| 927 | 909 | import * as subprocess from "#sitegen/subprocess"; |
| 928 | 910 | import { Path } from "#sitegen/path"; |
| 911 | import * as queue from "../../../lib/queue.ts"; |
| 912 | import * as term from "../../../lib/term.ts"; |
| 913 | import * as progress from "../../../lib/term/progress.ts"; |
| 914 | import * as ts from "../../../lib/ts.ts"; |
| 929 | 915 | |
| 930 | 916 | import * as child_process from "node:child_process"; |
| 931 | 917 | import * as crypto from "node:crypto"; |