Last active
May 6, 2024 09:08
-
-
Save lijunle/d714c8f4b2554c3e4acfa866605645e8 to your computer and use it in GitHub Desktop.
Rename DJI and iOS video files to normalized format.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'node:fs'; | |
import path from "node:path"; | |
import process from "node:process"; | |
import url from "node:url"; | |
if (!process.argv[2]) { | |
console.error(`Usage: node ${process.argv[1]} <media-folder> [shift-hours]`); | |
process.exit(-1); | |
} | |
const dirname = path.dirname(url.fileURLToPath(import.meta.url)); | |
const mediaFolder = path.resolve(dirname, process.argv[2]); | |
console.info("Media folder: " + mediaFolder); | |
const shiftHours = isNaN(parseInt(process.argv[3])) ? 0 : parseInt(process.argv[3]); | |
console.info("Shift Hours: " + shiftHours); | |
const files = fs.readdirSync(mediaFolder, "utf-8"); | |
for (let filename of files) { | |
processSonyPhotoFiles(filename, mediaFolder); | |
processSonyVideoFiles(filename, mediaFolder); | |
processIPhoneMediaFiles(filename, mediaFolder); | |
processDjiVideoFiles(filename, mediaFolder); | |
} | |
/** | |
* Process Sony photo files. | |
* @param {string} filename File name. | |
* @param {string} mediaFolder Media folder. | |
*/ | |
function processSonyPhotoFiles(filename, mediaFolder) { | |
if (filename.startsWith("_DSC") && filename.endsWith(".ARW")) { | |
console.log(`[${filename}] Identified as Sony photo file`); | |
const extname = path.extname(filename).toLowerCase(); | |
const filePath = path.resolve(mediaFolder, filename); | |
const fileStat = fs.statSync(filePath); | |
const targetFilename = formatDate(addHours(fileStat.mtime, shiftHours)) + extname; | |
console.log(`[${filename}] Rename to ${targetFilename}`); | |
renameSafely(filename, targetFilename, mediaFolder); | |
// If XMP file exists, rename it as well. | |
// BUG here: rename safely will end up with .arw.1.xmp which is incorrect. | |
const xmpFile = `${filename}.xmp`; | |
if (fs.existsSync(path.resolve(mediaFolder, xmpFile))) { | |
console.log(`[${filename}] Rename corresponding XMP file: ${xmpFile}`); | |
renameSafely(xmpFile, `${targetFilename}.xmp`, mediaFolder); | |
} | |
} | |
} | |
/** | |
* Process Sony video files. | |
* @param {string} filename File name. | |
* @param {string} mediaFolder Media folder. | |
*/ | |
function processSonyVideoFiles(filename, mediaFolder) { | |
if (filename.startsWith("C") && filename.endsWith(".MP4")) { | |
console.log(`[${filename}] Identified as Sony video file`); | |
const extname = path.extname(filename).toLowerCase(); | |
const filePath = path.resolve(mediaFolder, filename); | |
const fileStat = fs.statSync(filePath); | |
const targetFilename = formatDate(addHours(fileStat.mtime, shiftHours)) + extname; | |
console.log(`[${filename}] Rename to ${targetFilename}`); | |
fs.renameSync( | |
path.resolve(mediaFolder, filename), | |
path.resolve(mediaFolder, targetFilename) | |
); | |
// If XML file exists, rename it as well. | |
const xmlFile = filename.replace(/\.MP4$/, "M01.XML"); | |
if (fs.existsSync(path.resolve(mediaFolder, xmlFile))) { | |
console.log(`[${filename}] Rename corresponding XML file: ${xmlFile}`); | |
renameSafely(xmlFile, targetFilename.replace(/\.mp4$/, ".xml"), mediaFolder); | |
} | |
} | |
} | |
/** | |
* Process iPhone photo and video files. | |
* @param {string} filename File name. | |
* @param {string} mediaFolder Media folder. | |
*/ | |
function processIPhoneMediaFiles(filename, mediaFolder) { | |
const extname = path.extname(filename).toLowerCase(); | |
if ( | |
filename.startsWith("IMG_") && | |
['.mov', '.heic', '.jpg'].includes(extname) | |
) { | |
console.log(`[${filename}] Identified as iPhone media file`); | |
const filePath = path.resolve(mediaFolder, filename); | |
const fileStat = fs.statSync(filePath); | |
const targetFilename = formatDate(fileStat.mtime) + extname; | |
console.log(`[${filename} Rename to ${targetFilename}`); | |
renameSafely(filename, targetFilename, mediaFolder); | |
} | |
} | |
/** | |
* Process DJI video files. | |
* @param {string} filename File name. | |
* @param {string} mediaFolder Media folder. | |
*/ | |
function processDjiVideoFiles(filename, mediaFolder) { | |
if (filename.startsWith("DJI_")) { | |
console.log(`[${filename}] Identified as DJI video file`); | |
const { name, ext } = path.parse(filename); | |
const targetName = name.replace( | |
/DJI_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})_\d{3}_video/, | |
"$1-$2-$3 $4.$5.$6" | |
); | |
const targetedFilename = targetName + ext.toLowerCase(); | |
console.log(`[${filename}] Rename to ${targetedFilename}`); | |
renameSafely(filename, targetedFilename, mediaFolder); | |
} | |
} | |
/** | |
* Add hours to a date time | |
* @param {Date} date The date time to add hours | |
* @param {number} hour The number of hours to add | |
* @returns The new date time | |
*/ | |
function addHours(date, hour) { | |
const newDate = new Date(date); | |
newDate.setHours(date.getHours() + hour); | |
return newDate; | |
} | |
/** | |
* Format the date time to file name format. | |
* @param {Date} date The date time to format | |
* @returns The formated date time string | |
*/ | |
function formatDate(date) { | |
const year = date.getFullYear(); | |
const month = padZero(date.getMonth() + 1); | |
const day = padZero(date.getDate()); | |
const hour = padZero(date.getHours()); | |
const minute = padZero(date.getMinutes()); | |
const second = padZero(date.getSeconds()); | |
return `${year}-${month}-${day} ${hour}.${minute}.${second}`; | |
} | |
/** | |
* Pad zero to a number to be two digits. | |
* @param {number} number The number to pad zero | |
* @returns The padded number | |
*/ | |
function padZero(number) { | |
return number.toString().padStart(2, "0"); | |
} | |
/** | |
* Rename the old file name to new one. Stop if same name exists. | |
* @param {string} oldFilename Old file name. | |
* @param {string} newFilename New file name. | |
* @param {string} mediaFolder Media folder. | |
* @param {number} counterSuffix Count suffix to resolve file name conflict issue. | |
*/ | |
function renameSafely(oldFilename, newFilename, mediaFolder, counterSuffix) { | |
if (counterSuffix) { | |
const extname = path.extname(newFilename); | |
const basename = path.basename(newFilename, extname); | |
newFilename = `${basename} ${counterSuffix}${extname}`; | |
} | |
const oldPath = path.resolve(mediaFolder, oldFilename); | |
const newPath = path.resolve(mediaFolder, newFilename); | |
if (fs.existsSync(newPath)) { | |
const suffix = (counterSuffix || 0) + 1; | |
console.warn(`[${oldFilename}] New file name exist: ${newFilename}, try suffix: ${suffix}`); | |
renameSafely(oldFilename, newFilename, mediaFolder, suffix); | |
} else { | |
fs.renameSync(oldPath, newPath); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment