UNPKG

poto-siril

Version:

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

304 lines (268 loc) 9.19 kB
import path from "path"; import fs from "fs-extra"; import { ImageSpec, FileImageSpec } from "./types"; import { logger } from "./logger"; /** * Retrieve FITS files from a source directory. */ export const getFitsFromDirectory = ({ directory: directory, projectDirectory, }: { directory: string; projectDirectory: string; }) => { const files: fs.Dirent[] = fs.readdirSync(directory, { recursive: true, withFileTypes: true, encoding: "utf8", }); const fileImageSpecs: FileImageSpec[] = []; let previousFile: FileImageSpec | null = 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: fs.Dirent, projectDirectory: string, previousFile: FileImageSpec | null, ): FileImageSpec => { // 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), } as FileImageSpec; 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: string): Date => { 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: Date): string => { const pad = (num: number) => 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: string): number => { 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: FileImageSpec, alreadyImported: FileImageSpec[], ): FileImageSpec[] => { 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: ImageSpec, B: ImageSpec): boolean => { 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: FileImageSpec): string => { 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: string): ImageSpec => { 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", "")), } as ImageSpec) : ({ setName, type: setName.split("_")[0], bulb: setName.split("_")[1], bin: setName.split("_")[2], filter: null, gain: Number(setName.split("_")[3].replace("gain", "")), } as ImageSpec); }; /** * Detect if ASIAIR directory structure is present. */ export const isAsiAirDirectoryF = ( directory: string, ): { isAsiAirDirectory: boolean; autorunDirectory: string; planDirectory: string; } => { const autorunDirectory = `${directory}/Autorun`; const planDirectory = `${directory}/Plan`; return { isAsiAirDirectory: fs.existsSync(autorunDirectory) || fs.existsSync(planDirectory), autorunDirectory, planDirectory, }; };