UNPKG

poto-siril

Version:

Automatization around Siril (<https://siril.org/>) for deep sky astrophotography.

237 lines (236 loc) 9.19 kB
import path from "path"; import fs from "fs-extra"; import { logger } from "./logger.js"; /** * Retrieve FITS files from a source directory. */ export const getFitsFromDirectory = ({ directory: directory, projectDirectory, }) => { const files = fs.readdirSync(directory, { recursive: true, withFileTypes: true, encoding: "utf8", }); const fileImageSpecs = []; let previousFile = null; // Process the list of files. files.forEach(file => { if (file.isDirectory()) { return; } if (file.name.endsWith("_thn.jpg")) { logger.debug(`Skipping thumbnail file (${file.name})`); return; } if (!(file.isFile() || file.isSymbolicLink())) { logger.debug(`Skipping not-file or symlink (${file.name})`); return; } // Ignore macOS files. if (file.name === ".DS_Store" || file.name.startsWith("._") || file.parentPath.includes("._")) { return; } // Ignore Windows files. if (file.name === "Thumbs.db") { return; } if (!file.name.endsWith(".fit")) { logger.debug(`Skipping unknown file (${file.name})`); return; } // If the file is a FITS file, process it. const specs = getFileImageSpecFromFilename(file, projectDirectory, previousFile); previousFile = specs; fileImageSpecs.push(specs); }); return fileImageSpecs; }; /** * Retrieve Light / Flat, Bulb duratin, binning, filter, gain, date, time, temperature, frame number in the sequence, sequence unique identifier. * @param filename */ export const getFileImageSpecFromFilename = (fileFS, projectDirectory, previousFile) => { // Light_LDN 1093_120.0s_Bin1_H_gain100_20240707-002348_-10.0C_0001.fit // Flat_810.0ms_Bin1_H_gain0_20240707-102251_-9.9C_0019.fit // Dark_300.0s_Bin1_L_gain360_20230910-101917_-9.8C_0001.fit // Bias_1.0ms_Bin1_L_gain100_20240308-154938_-9.9C_0003.fit // Note: Filter is not always specified in the filename. const regex = /^(?<type>[A-Za-z]+)(?:_[^_]+)?_(?<bulb>[^_]+)_(?<bin>Bin\d)_((?<filter>[A-Za-z0-9 ]+)_)?gain(?<gain>\d+)_(?<datetime>\d{8}-\d{6})_(?<temperature>-?\d+\.\d+C)_(?<sequence>\d{4})\.(?<extension>fit)$/; const match = fileFS.name.match(regex); if (match && match.groups) { const sequencePosition = parseInt(match.groups.sequence, 10); const datetime = parseDate(match.groups.datetime); const temperature = parseFloat(match.groups.temperature); const file = { setName: "", type: match.groups.type, bulb: match.groups.bulb, bulbMs: parseBulbString(match.groups.bulb), bin: match.groups.bin, filter: match.groups.filter?.replaceAll(" ", "").trim() ?? null, gain: parseInt(match.groups.gain, 10), sequenceId: "", // To be determined later. Referenced here early to have serialization printing fields in this order. sequencePosition, datetime, temperature, fileName: fileFS.name, extension: match.groups.extension, sourceFileDirectory: fileFS.parentPath, sourceFilePath: path.join(fileFS.parentPath, fileFS.name), }; file.setName = getSetName(file); if (!previousFile) { file.sequenceId = unParseDate(datetime); } else { // Is considered part of the same sequence if the previous file is of the same type and the sequence position is bigger (not only by 1 to allow sequences with holes in it.). file.sequenceId = previousFile.setName === file.setName && previousFile.sequencePosition < sequencePosition ? previousFile.sequenceId : unParseDate(datetime); } if (file.type === "Light") { file.projectFileDirectory = undefined; // To be determined later from layerSetId. Referenced here early to have serialization printing fields in this order. } else if (file.type === "Flat") { const directory = file.setName; file.projectFileDirectory = file.filter ? path.join(projectDirectory, file.filter, directory) : path.join(projectDirectory, directory); } else { const directory = `${file.type}_${file.bulb}_${file.bin}_gain${file.gain}`; file.projectFileDirectory = path.join(projectDirectory, "any", directory); // We ignore the filter for darks and biases. } if (file.type === "Light") { file.projectFilePath = undefined; // To be determined later from layerSetId. Referenced here early to have serialization printing fields in this order. } else { file.projectFilePath = path.join(file.projectFileDirectory, file.fileName); } return file; } else { throw new Error(`Filename ${fileFS.name} does not match the expected pattern for Specs extraction.`); } }; /** * @param datetimeString `20240707-002348` format. */ const parseDate = (datetimeString) => { const datetimeRegex = /(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/; const matchResult = datetimeString.match(datetimeRegex); if (matchResult) { const [_, year, month, day, hour, minute, second] = matchResult; const parsedDate = new Date(parseInt(year, 10), parseInt(month, 10) - 1, // Month is zero-based in JavaScript Date. parseInt(day, 10), parseInt(hour, 10), parseInt(minute, 10), parseInt(second, 10)); return parsedDate; } else { throw new Error(`Invalid datetime string: ${datetimeString}`); } }; /** * Inverse function of `parseDate`. * @returns `20240707-002348` format. */ const unParseDate = (datetime) => { const pad = (num) => num.toString().padStart(2, "0"); return `${datetime.getFullYear()}${pad(datetime.getMonth() + 1)}${pad(datetime.getDate())}-${pad(datetime.getHours())}${pad(datetime.getMinutes())}${pad(datetime.getSeconds())}`; }; /** * * @param bulbString `120.0s` or `810.0ms`. * @returns 120000 or 810 (in ms). */ const parseBulbString = (bulbString) => { const bulbRegex = /(\d+\.\d+)(ms|s)/; const matchResult = bulbString.match(bulbRegex); if (matchResult) { const [_, bulb, unit] = matchResult; return unit === "s" ? parseFloat(bulb) * 1000 : parseFloat(bulb); } else { throw new Error(`Invalid bulb string: ${bulbString}`); } }; export const copyFileToProject = (file, alreadyImported) => { if (!fs.existsSync(file.projectFileDirectory)) { fs.mkdirSync(file.projectFileDirectory, { recursive: true }); } if (alreadyImported.find(f => f.fileName === file.fileName)) { logger.debug(`- ${file.fileName} already imported.`); } else { fs.copyFileSync(file.sourceFilePath, file.projectFilePath); alreadyImported.push(file); logger.debug(`- ${file.fileName} imported.`); } return alreadyImported; }; /** * Used to match flats with biases, lights with darks. * Allow these couples: * - [A] with [B] * - Light with Dark. * - Light with Flat (allowing for different gain). * - Flat with Bias. */ export const matchSetFile = (A, B) => { if (A.type === "Light" && B.type === "Dark") { return A.bulb === B.bulb && A.bin === B.bin && A.gain === B.gain; } else if (A.type === "Light" && B.type === "Flat") { return A.bin === B.bin && A.filter === B.filter; } else if (A.type === "Flat" && B.type === "Bias") { return A.bin === B.bin && A.gain === B.gain; } else { return false; } }; /** * @returns `Flat_520.0ms_Bin1_H_gain0`, `Flat_520.0ms_Bin1_gain0` format. */ const getSetName = (file) => { return file.filter ? `${file.type}_${file.bulb}_${file.bin}_${file.filter}_gain${file.gain}` : `${file.type}_${file.bulb}_${file.bin}_gain${file.gain}`; }; /** * Utils for map. */ export const getImageSpecFromSetName = (setName) => { return setName.split("_").length === 5 ? { setName, type: setName.split("_")[0], bulb: setName.split("_")[1], bin: setName.split("_")[2], filter: setName.split("_")[3], gain: Number(setName.split("_")[4].replace("gain", "")), } : { setName, type: setName.split("_")[0], bulb: setName.split("_")[1], bin: setName.split("_")[2], filter: null, gain: Number(setName.split("_")[3].replace("gain", "")), }; }; /** * Detect if ASIAIR directory structure is present. */ export const isAsiAirDirectoryF = (directory) => { const autorunDirectory = `${directory}/Autorun`; const planDirectory = `${directory}/Plan`; return { isAsiAirDirectory: fs.existsSync(autorunDirectory) || fs.existsSync(planDirectory), autorunDirectory, planDirectory, }; };