最近沉迷漫画,收集了一堆野生资源,偶尔会遇到一些四格漫画,观看体验不是很好,因为每话才几页,就独立成了一个目录,比如这样:
然后我就下意识的想写个脚本解决这个问题,操作本地文件是用很多工具都可以实现的,由于当初在南非挖矿的时候入门编程学的是py,我第一时间是想用py的os模块去实现文件和目录的操作,一想上一次用py是快三年前的事情了,很多东西忘得差不多,包括前段时间换了新电脑,也没装有py环境,所以还是选择了更熟悉也不需要额外安装的node。
首先是明确了功能是在不改变源目录中文件顺序的情况下将多个目录按文件名顺序合并到新目录,以这样的方式手动制作分卷。由于涉及到复制文件、创建目录,所以核心功能需要通过fs模块来完成;如果是自己用的话,一些比如输入输出目录这种配置参数,大可直接写死在代码里,需要用的时候再改就是了,但为了提高灵活性吧,所以我希望能像其他的CLI工具一样,通过用户输入来决定配置,这需要用到readline模块来获取用户在命令行中的输入(这里会比py麻烦点,py一个input方法就可以获取用户在命令行里的输入了)
接下来直接上代码:
const fs = require('fs');
const readline = require('readline');/**
* @description 获取questtion的返回
* @param {String} question 用户提示
* @param {Function} handler 验证用户输入
* @returns {Promise} rl.question方法本身是通过回调来处理用户输入的,所以选择了返回promise来做阻塞,有序地抛出question并接收answer
*/
function getQuestionResult(question,handler){
return new Promise((res)=>{
// 创建一个可读流,用来读取在cmd中的输入
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
/**
* 考虑到即使用户输入异常,question方法都会在监听到换行之后结束,所以把handleResult的结构设计成一个对象,
* 由一个状态值success来表示是否通过handler的校验,success为false时应再次执行并且获取用户输入 */
rl.question(question, async (ans)=>{
const handleResult = handler(ans)
if (handleResult.success){
rl.close()
res(handleResult.value)
} else {
//handleResult.success为false时,handleResult.value是handler中设置的错误提示
rl.close()
const rejecthandle = await getQuestionResult(handleResult.value,handler)
res(rejecthandle)
}
})
})
}
// 这里创建一个generator实例,我觉得generator的yield单向有序的特性很适合我这个需求
function*gen(){ // make questions
function getChangeSettingsFlag(ans){
return ans==="Y"?{success:true,value:true}:{success:true,value:false}
}
const getPath = function (ans) {
/** * @description 验证路径(是否是目录) * @param {String} path */
const validatePath = function (path) {
try {
// node10.x及以下版本不支持readdirSync
const dir = fs.readdirSync(path) if (dir) {
return {success:true,value:path}
}
} catch(err){
return {success:false,value:"提供的地址不合理,请重新输入:"}
}
}
return validatePath(ans)
}
// yield并不会返回值,这里声明的changeSettingFlag的值实际上是接收的next方法的参数
const changeSettingFlag = yield getChangeSettingsFlag
const settings = {
volumeSize: 10,
dirName: "新建分卷"
}
// 改变预设
if (changeSettingFlag){
const volumeSize = yield function(volumeSize){
return Number(volumeSize)>0&&Number(volumeSize)!==Infinity?{success:true,value:volumeSize}:{success:false,value:"输入的数字不合理,请重新输入:"}
}
settings.volumeSize = Number(volumeSize)||settings.volumeSize
const dirName = yield function(dirName){
return dirName.trim()?{success:true,value:dirName}:{success:false,value:"输入的目录名不合理,请重新输入:"}
}
settings.dirName = dirName.trim()||settings.dirName
}
// 接收路径
const pathInfo = { input: "", output: "" }
const inputPath = yield getPath
pathInfo.input = inputPath;
const outputPath = yield getPath
pathInfo.output = outputPath;
const conf = { pathInfo, settings }
console.log("conf",conf)
yield conf
}
// run it
async function workflow(generator){
const func0 = generator.next().value
const changeSettingFlag = await getQuestionResult("当前预设置如下:\n\t输出的分卷名:“新建分卷”;\n\t容量:10话/卷;\n希望调整预设吗?(Y/n) ",func0)
let getvolumeSize,getdirName
if (changeSettingFlag){
const func1 = generator.next(changeSettingFlag).value
getvolumeSize = await getQuestionResult("期望的卷容量(话/卷)是: ",func1)
const func2 = generator.next(getvolumeSize).value
getdirName = await getQuestionResult("期望的分卷名是:",func2)
}
const func3 = generator.next(getdirName).value
const inputPath = await getQuestionResult("选择的源路径是:",func3)
const func4 = generator.next(inputPath).value
const outputPath = await getQuestionResult("期望的输出路径是:",func4)
const conf = generator.next(outputPath).value
// 当前计数,通过在文件名中count来保持排序
let currentCount = 0;
// 当前分卷
let currentVolume = 0;
/**
* @param {String} path
* @param {String} newFolderName
*/
async function letsdance(path,newFolderName="") {
const childs = fs.opendirSync(path)
let chunkNum = 0 let newFolderPath = newFolderName
for await (const dirent of childs) {
if (dirent.isDirectory()) {
// 填充满一个目录之后创建一个新目录
if (currentVolume%conf.settings.volumeSize===0) {
chunkNum+=1;
newFolderPath = conf.pathInfo.output+"/"+conf.settings.dirName+"_"+chunkNum
fs.mkdirSync(newFolderPath)
// 如果你不希望文件名一直递增,你可以在新建目录之后把currentCount重新置为0
}
currentVolume+=1
const nextLevelPath = path+"/"+dirent.name
letsdance(nextLevelPath,newFolderPath)
} else if (dirent.isFile()){
currentCount += 1
const extName = dirent.name.split(".").reverse()[0]
const targetFilePath = path+"/"+dirent.name
const newFileName = newFolderPath+"/"+currentCount+"."+extName
fs.copyFileSync(targetFilePath,newFileName)
}
}
}
letsdance(conf.pathInfo.input)
}
const g = gen()
workflow(g)
效果: