NodeJs 获取照片拍摄日期和视频拍摄日期,并按日期目录存档
需安装依赖模块:
npm install exif
npm install ffprobe
npm install ffprobe-static
npm install pify
功能代码献上(photo-archives.js)
// 文件路径:photo-archives.js
/**
* NodeJs 获取照片拍摄日期和视频拍摄日期,并按日期目录存档
* 功能:NodeJs 获取照片拍摄日期和视频拍摄日期,并按日期目录存档
* 使用:node photo-archives.js
* 扩展包:
* npm install exif
* npm install ffprobe
* npm install ffprobe-static
* npm install pify
*/
// 引用 fs 文件系统模块
const NmFs = require('fs')
// 引用 path 路径处理模块
const NmPath = require('path');
// 安装并引用 exif 图像元数据处理模块
const NmExif = require('exif')
// 安装并引用 ffprobe 视频元数据处理模块
const NmFfprobe = require('ffprobe')
const NmFfprobeStatic = require('ffprobe-static');
// 安装并引用 Promise 自动转换回调方法模块
const NmPify = require('pify');
// 配置信息
const config = {
image_exts: ['jpg', 'png', 'gif', 'jpeg', 'webp', 'tiff'],
video_exts: ['mp4', 'mov'],
}
/**
* 移动照片和视频到日期目录
* @param {String} fromDir 来源目录
* @param {String} toDir 目标目录
* @param {Boolean} isDebug 是否调试模式。调试模式只会在控制台输出信息,不会真正操作文件
* @param {Boolean} isSkipExists 是否跳过已存在的目标文件
*/
async function movePhotos(fromDir, toDir, isDebug = true, isSkipExists = true) {
if (!NmFs.existsSync(fromDir)) {
console.log('path not exists: ', fromDir);
return;
}
// 自动补齐路径符
const SEP = NmPath.sep;
if (!fromDir.endsWith(SEP)) {
fromDir += SEP;
}
if (!toDir.endsWith(SEP)) {
toDir += SEP;
}
// 打开目录
const dir = await NmFs.promises.opendir(fromDir);
// 声明变量,优化内存
let ext = '',
prefix = '',
newDir = '',
newPath = '',
currentPath = '',
stat = null,
datestr = '',
time = 0,
date = null,
exifData = null;
for await (const dirent of dir) {
// 当前路径
currentPath = fromDir + dirent.name;
// 处理目录
if (dirent.isDirectory()) {
// 如果当前路径是目录,则进入递归模式
movePhotos(currentPath + SEP, toDir, isDebug, isSkipExists);
continue;
}
// 处理文件
ext = NmPath.extname(dirent.name); // .jpg
if (!ext) {
continue;
}
date = null;
ext = ext.substring(1).toLowerCase();
if (config.image_exts.includes(ext)) {
prefix = 'IMG-';
} else if (config.video_exts.includes(ext)) {
prefix = 'MOV-';
} else {
// 过滤非图片和视频格式的文件
continue;
}
if (ext == 'jpg' || ext == 'jpeg') {
exifData = await NmPify(NmExif.ExifImage)({
image: currentPath
});
// 2020:04:04 08:34:33
datestr = exifData.exif.CreateDate.replace(':', '-').replace(':', '-').replace(' ', 'T') + 'Z';
date = new Date(datestr); // 注意,时间后面要带字符Z,否则会差8小时,详见:https://segmentfault.com/q/1010000011751536
// console.log('jpeg===', currentPath, datestr);
}
if (ext == 'mov' || ext == 'mp4') {
exifData = await NmFfprobe(currentPath, {
path: NmFfprobeStatic.path
})
// creation_time: '2020-04-04T00:34:32.000000Z'
datestr = exifData.streams[0].tags.creation_time.substring(0, 19) + 'Z';
date = new Date(datestr);
// console.log('video===', currentPath, datestr);
}
if (!date) {
stat = NmFs.statSync(currentPath);
time = stat.birthtime > stat.ctime ? stat.ctime : stat.birthtime;
if (time > stat.mtime) {
time = stat.mtime;
}
// 时间后面要加字符'Z',否则会少8小时
date = new Date(time + 'Z');
// console.log('other===', currentPath, date.toISOString());
}
// 自动创建目标目录
newDir = toDir + date.toISOString().substring(0, 10);
if (!isDebug && !NmFs.existsSync(newDir)) {
NmFs.mkdirSync(newDir, {
recursive: true
});
}
newPath = date.toISOString().replace(/-/g, '').replace(/:/g, '').substring(0, 15);
newPath = newDir + '/' + prefix + newPath + '.' + ext; // IMG-20210117113520.jpg
// 判断是否过滤已存在的目标文件
if (isSkipExists && NmFs.existsSync(newPath)) {
console.log(currentPath, '已存在', newPath);
continue;
}
// 文件重命名
if (!isDebug) {
NmFs.renameSync(currentPath, newPath);
}
console.log(currentPath, '移动到', newPath);
}
}
// 执行批量照片和视频归档功能
movePhotos('./a/', './b/', true).catch(err => console.log(err))
// 文件时间调试
function debugFileTime(stat) {
let options = {
birthtime: '',
ctime: '',
mtime: '',
atime: ''
}
for (let key in options) {
if (stat[key]) {
options[key] = new Date(stat[key] + 'Z').toISOString();
}
}
console.log('debugFileTime', options);
}
运行:
node photo-archives.js