├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── config.json ├── electron ├── .gitignore ├── .npmrc ├── build.sh ├── index.html ├── main.js ├── package.json ├── preload.js ├── readme.md ├── renamer.ico └── yarn.lock ├── openRenamerBackend ├── .gitignore ├── .idea │ ├── .gitignore │ ├── modules.xml │ ├── openRenamerBackend.iml │ └── vcs.xml ├── api │ ├── ApplicationRuleApi.ts │ ├── AutoPlanApi.ts │ ├── FileApi.ts │ ├── GlobalConfigApi.ts │ ├── PublicApi.ts │ ├── QbServiceApi.ts │ └── RenamerApi.ts ├── config.ts ├── dao │ ├── ApplicationRuleDao.ts │ ├── GlobalConfigDao.ts │ └── SavePathDao.ts ├── entity │ ├── bo │ │ └── rules │ │ │ ├── AutoRule.ts │ │ │ ├── DeleteRule.ts │ │ │ ├── InsertRule.ts │ │ │ ├── ReplaceRule.ts │ │ │ ├── RuleInterface.ts │ │ │ ├── SerializationRule.ts │ │ │ └── TranslateRole.ts │ ├── constants │ │ └── GlobalConfigCodeConstant.ts │ ├── dto │ │ ├── AutoPlanConfigDto.ts │ │ ├── BtListItemDto.ts │ │ └── QbConfigDto.ts │ ├── po │ │ ├── ApplicationRule.ts │ │ ├── GlobalConfig.ts │ │ └── SavePath.ts │ └── vo │ │ ├── FileObj.ts │ │ └── RuleObj.ts ├── index.ts ├── middleware │ ├── controllerEngine.ts │ ├── handleError.ts │ └── init.ts ├── package.json ├── pnpm-lock.yaml ├── service │ ├── ApplicationRuleService.ts │ ├── AutoPlanService.ts │ ├── FileService.ts │ ├── GlobalConfigService.ts │ ├── QbService.ts │ └── RenamerService.ts ├── sqls │ ├── v001__init.sql │ ├── v002__init.sql │ ├── v003_新增默认模板.sql │ ├── v004_新增记录表.sql │ └── v005_新增推荐规则.sql ├── start.sh ├── static │ └── .gitkeep ├── tsconfig.json └── util │ ├── ErrorHelper.ts │ ├── LogUtil.ts │ ├── MediaUtil.ts │ ├── NumberUtil.ts │ ├── ObjectOperate.ts │ ├── ProcesHelper.ts │ ├── QbApiUtil.ts │ ├── SqliteHelper.ts │ ├── TimeUtil.ts │ ├── TranslateUtil.ts │ ├── TranslateWord.ts │ ├── ValUtil.ts │ └── pathUtil.ts └── openRenamerFront ├── .browserslistrc ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── FileChose.vue │ ├── Rule.vue │ ├── Tips.vue │ └── rules │ │ ├── ApplicationRuleList.vue │ │ ├── AutoRule.vue │ │ ├── DeleteRule.vue │ │ ├── InsertRule.vue │ │ ├── ReplaceRule.vue │ │ ├── RuleBlock.vue │ │ ├── SerializationRule.vue │ │ └── TranslateRule.vue ├── main.js ├── router │ └── index.js ├── utils │ ├── Bus.js │ ├── HttpUtil.js │ └── ValUtil.js └── views │ ├── auto │ ├── components │ │ └── editForm.vue │ └── index.vue │ ├── download │ └── config │ │ └── index.vue │ ├── home │ └── Home.vue │ └── public │ └── login.vue └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | data 107 | /.idea/.gitignore 108 | /.idea/modules.xml 109 | /.idea/open-renamer.iml 110 | /.idea/vcs.xml 111 | 112 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-buster-slim 2 | WORKDIR /app 3 | COPY ./openRenamerBackend /app 4 | # RUN chmod 777 -R /app && npm install -g pnpm typescript --registry https://registry.npmmirror.com 5 | # 注意此处未添加npm代理 6 | RUN chmod 777 -R /app && npm install -g pnpm typescript 7 | ENV PORT 80 8 | CMD ["bash", "start.sh"] 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-renamer 2 | 3 | ![预览图](https://s3.fleyx.com/picbed/2022/11/18386180128d01eb1a59b8eacf652895.png) 4 | 5 | renamer 的开源实现版本,BS 应用,支持 arm/x86 部署使用,两种使用方式: 6 | 7 | 1. 部署容器到 nas 8 | 已打包到 dockerhub 中:[hub.docker.com/r/fleyx/open-renamer](https://hub.docker.com/r/fleyx/open-renamer) 9 | 10 | 2. 下载桌面应用使用,目前仅支持 windows,后续计划支持 mac,linux[下载地址](https://github.com/FleyX/open-renamer/releases/latest) 11 | 12 | [点击查看参考文档](https://blog.fleyx.com/blog/detail/20221130) 13 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | base=$(cd "$(dirname "$0")";pwd) 3 | cd $base 4 | rm -rf openRenamerBackend/dist 5 | # 注意此处未添加npm代理 6 | # docker run -it --rm --name buildOpenRenamer --user ${UID} -v $base/openRenamerFront:/opt/front node:lts-slim bash -c "cd /opt/front && npm install -g pnpm --registry https://registry.npmmirror.com && pnpm install --registry https://registry.npmmirror.com && pnpm run build" 7 | docker run -it --rm --name buildOpenRenamer --user ${UID} -v $base/openRenamerFront:/opt/front node:lts-slim bash -c "cd /opt/front && npm install -g pnpm && pnpm install && pnpm run build" 8 | 9 | rm -rf openRenamerBackend/static/* 10 | touch openRenamerBackend/static/.gitkeep 11 | mv openRenamerFront/dist/* openRenamerBackend/static 12 | rm -rf openRenamerBackend/node_modules 13 | 14 | # 单平台打包并推送 15 | #docker build -t fleyx/open-renamer:$0 --push . 16 | # 多平台打包并推送 17 | docker buildx build -t fleyx/open-renamer:$1 --platform linux/amd64,linux/arm64 --push . 18 | docker buildx build -t fleyx/open-renamer:latest --platform linux/amd64,linux/arm64 --push . 19 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":1.3 3 | } -------------------------------------------------------------------------------- /electron/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | openRenamerBackend 4 | build 5 | .idea 6 | *.exe -------------------------------------------------------------------------------- /electron/.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ -------------------------------------------------------------------------------- /electron/build.sh: -------------------------------------------------------------------------------- 1 | cd ../openRenamerFront 2 | yarn 3 | npm run build 4 | cd ../openRenamerBackend 5 | yarn 6 | tsc 7 | rm -rf ./static/js 8 | rm -rf ./static/css 9 | cp -r ../openRenamerFront/dist/* ./static 10 | cd ../electron 11 | mkdir -p dist 12 | npm run build -------------------------------------------------------------------------------- /electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | open-renamer 4 | 5 | 6 |
7 |
8 |
loading...
9 |
10 | 11 | 12 | 58 | 69 | 70 | -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | // main.js 2 | // 控制应用生命周期和创建原生浏览器窗口的模组 3 | const {app, BrowserWindow, Menu} = require('electron') 4 | const path = require('path') 5 | const fs = require('fs'); 6 | const {spawn} = require('child_process'); 7 | const net = require('net'); 8 | const log = require('electron-log'); 9 | 10 | 11 | const userHome = process.env.HOME || process.env.USERPROFILE; 12 | const dataPath = path.join(userHome, "openRenamer"); 13 | 14 | log.transports.file.resolvePathFn = () => path.join(dataPath, 'logs/main.log'); 15 | 16 | async function createWindow() { 17 | // 隐藏菜单栏 18 | Menu.setApplicationMenu(null) 19 | // 创建浏览器窗口 20 | const win = new BrowserWindow({ 21 | //width: 800, //窗口宽度,单位像素. 默认是 800 22 | //height: 600, //窗口高度,单位像素. 默认是 600 23 | icon: './logo.ico', // 设置窗口左上角的图标 24 | show: false, //窗口创建的时候是否显示. 默认为 true 25 | webPreferences: { 26 | nodeIntegration: true, // 是否完整支持node。默认为 true 27 | preload: path.join(__dirname, 'preload.js') //界面的其它脚本运行之前预先加载一个指定脚本。 28 | } 29 | }); 30 | //打开调试 31 | // win.webContents.openDevTools(); 32 | 33 | win.loadFile('./index.html'); 34 | let startTime = Date.now(); 35 | 36 | // 下面这两行代码配合上面 new BrowserWindow 里面的 show: false,可以实现打开时窗口最大化 37 | win.maximize() 38 | win.show() 39 | log.info(__dirname); 40 | let port = await startBackend() 41 | log.info("backend service started") 42 | 43 | let diff = Date.now() - startTime; 44 | let time = 2000; 45 | if (diff < time) { 46 | await sleep(time - diff); 47 | } 48 | win.loadURL(`http://localhost:` + port); 49 | // win.webContents.openDevTools() 50 | } 51 | 52 | // Electron会在初始化完成并且准备好创建浏览器窗口时调用这个方法 53 | // 部分 API 在 ready 事件触发后才能使用。 54 | app.whenReady().then(createWindow) 55 | // 当所有窗口都被关闭后退出 56 | app.on('windows-all-closed', () => { 57 | // 在 macOS 上,除非用户用 Cmd + Q 确定地退出, 58 | // 否则绝大部分应用及其菜单栏会保持激活。 59 | if (process.platform !== 'darwin') { 60 | app.quit() 61 | } 62 | }) 63 | app.on('activate', () => { 64 | // 在macOS上,当单击dock图标并且没有其他窗口打开时, 65 | // 通常在应用程序中重新创建一个窗口。 66 | if (BrowserWindow.getAllWindows().length === 0) { 67 | createWindow() 68 | } 69 | }) 70 | 71 | /** 72 | * 启动后台服务 73 | * @returns {Promise} 74 | */ 75 | async function startBackend() { 76 | let port = 51000; 77 | while (true) { 78 | let ok = await checkPort(port); 79 | if (ok) { 80 | break; 81 | } 82 | port = port + 1; 83 | } 84 | log.info("start check folder exist", __dirname, __filename) 85 | let exist = fs.existsSync("openRenamerBackend"); 86 | const childProcess = spawn('node', [(exist ? '' : '../') + 'openRenamerBackend/dist/index.js'], { 87 | env: { 88 | "PORT": port, 89 | "DATA_PATH": dataPath 90 | } 91 | }); 92 | 93 | childProcess.stdout.on('data', (data) => { 94 | log.info(`stdout: ${data}`); 95 | }); 96 | 97 | childProcess.stderr.on('data', (data) => { 98 | log.error(`stderr: ${data}`); 99 | }); 100 | 101 | childProcess.on('close', (code) => { 102 | log.info(`child process exited with code ${code}`); 103 | }); 104 | log.info("check service start"); 105 | while (true) { 106 | await sleep(100); 107 | let success = !(await checkPort(port)); 108 | if (success) { 109 | log.info("service start"); 110 | break; 111 | } 112 | } 113 | return port; 114 | } 115 | 116 | /** 117 | * 判断端口是否可用 118 | * @param port 119 | * @returns {Promise} 120 | */ 121 | function checkPort(port) { 122 | return new Promise((resolve, reject) => { 123 | let server = net.createServer().listen(port); 124 | server.on("listening", function () { 125 | server.close(); 126 | resolve(true); 127 | }) 128 | server.on("error", function (err) { 129 | console.error(err); 130 | resolve(false); 131 | }) 132 | }) 133 | } 134 | 135 | function sleep(time) { 136 | return new Promise((resolve, reject) => { 137 | setTimeout(() => resolve(), time); 138 | }) 139 | } -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renamer", 3 | "version": "1.8.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "electron-builder --win --x64" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "electron": "^28.0.0", 15 | "electron-builder": "^24.9.1" 16 | }, 17 | "build": { 18 | "productName": "openRenamer", 19 | "appId": "openRenamer.app", 20 | "directories": { 21 | "output": "build" 22 | }, 23 | "files": [ 24 | "main.js", 25 | "preload.js", 26 | "index.html" 27 | ], 28 | "extraFiles": [ 29 | { 30 | "from": "../openRenamerBackend", 31 | "to": "openRenamerBackend" 32 | } 33 | ], 34 | "copyright": "open-renamer", 35 | "nsis": { 36 | "oneClick": false, 37 | "allowElevation": true, 38 | "allowToChangeInstallationDirectory": true, 39 | "installerIcon": "./renamer.ico", 40 | "uninstallerIcon": "./renamer.ico", 41 | "installerHeaderIcon": "./renamer.ico", 42 | "createDesktopShortcut": true, 43 | "createStartMenuShortcut": true, 44 | "shortcutName": "openRenamer" 45 | }, 46 | "win": { 47 | "icon": "./renamer.ico", 48 | "target": [ 49 | "nsis", 50 | "zip" 51 | ], 52 | "extraFiles": [ 53 | { 54 | "from": "windows/node.exe", 55 | "to": "node.exe" 56 | } 57 | ] 58 | }, 59 | "mac": { 60 | "target": [ 61 | "dmg", 62 | "zip" 63 | ] 64 | }, 65 | "linux": { 66 | "icon": "build/icons" 67 | } 68 | }, 69 | "dependencies": { 70 | "electron-log": "^5.0.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /electron/preload.js: -------------------------------------------------------------------------------- 1 | // preload.js 2 | // 所有Node.js API都可以在预加载过程中使用。 3 | // 它拥有与Chrome扩展一样的沙盒。 4 | window.addEventListener('DOMContentLoaded', () => { 5 | const replaceText = (selector, text) => { 6 | const element = document.getElementById(selector) 7 | if (element) element.innerText = text 8 | } 9 | for (const dependency of ['chrome', 'node', 'electron']) { 10 | replaceText(`${dependency}-version`, process.versions[dependency]) 11 | } 12 | }) -------------------------------------------------------------------------------- /electron/readme.md: -------------------------------------------------------------------------------- 1 | 需要下载windows版的node.js压缩包,将其中的node.exe 放到windowes目录下 -------------------------------------------------------------------------------- /electron/renamer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/51c6ec998677e02fe4973d7246d4cacc9d64f53a/electron/renamer.ico -------------------------------------------------------------------------------- /openRenamerBackend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.vscode/ 3 | .vscode 4 | node_modules/ 5 | /dist/ 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | yarn.lock 12 | log 13 | sqls/history.json 14 | static/* 15 | !static/.gitkeep 16 | database.db 17 | .idea -------------------------------------------------------------------------------- /openRenamerBackend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /openRenamerBackend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /openRenamerBackend/.idea/openRenamerBackend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /openRenamerBackend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /openRenamerBackend/api/ApplicationRuleApi.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "koa"; 2 | import ApplicationRuleService from "../service/ApplicationRuleService"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 获取目录下的文件列表 8 | */ 9 | router["GET /applicationRule"] = async function (ctx: Context) { 10 | ctx.body = await ApplicationRuleService.getAll(); 11 | }; 12 | 13 | /** 14 | * 获取默认模板 15 | */ 16 | router["GET /applicationRule/default"] = async function (ctx: Context) { 17 | ; 18 | ctx.body = await ApplicationRuleService.getDefault(); 19 | }; 20 | 21 | /** 22 | * 更新或者插入 23 | */ 24 | router['POST /applicationRule'] = async function (ctx: Context) { 25 | ctx.body = await ApplicationRuleService.saveOrAdd(ctx.request.body); 26 | } 27 | 28 | /** 29 | * 删除 30 | */ 31 | router["DELETE /applicationRule/:id"] = async function (ctx: Context) { 32 | await ApplicationRuleService.deleteById(ctx.params.id); 33 | ctx.body = ""; 34 | }; 35 | 36 | 37 | 38 | export default router; 39 | -------------------------------------------------------------------------------- /openRenamerBackend/api/AutoPlanApi.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "koa"; 2 | import AutoPlanService from "../service/AutoPlanService"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 获取目录下的文件列表 8 | */ 9 | router["POST /autoPlan/save"] = async function (ctx: Context) { 10 | ctx.body = await AutoPlanService.saveAutoConfig(ctx.request.body); 11 | }; 12 | 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /openRenamerBackend/api/FileApi.ts: -------------------------------------------------------------------------------- 1 | import {Context} from "koa"; 2 | import FileService from "../service/FileService"; 3 | import config from "../config"; 4 | 5 | const router = {}; 6 | 7 | /** 8 | * 获取目录下的文件列表 9 | */ 10 | router["GET /file/query"] = async function (ctx: Context) { 11 | ctx.body = await FileService.readPath(ctx.query.path as string, ctx.query.showHidden === '1'); 12 | }; 13 | 14 | /** 15 | * 递归读取文件夹下所有的文件 16 | */ 17 | router["POST /file/recursionQuery"] = async function (ctx: Context) { 18 | ctx.body = await FileService.readRecursion(ctx.request.body); 19 | }; 20 | 21 | /** 22 | *是否windows 23 | */ 24 | router['GET /file/isWindows'] = async function (ctx: Context) { 25 | ctx.body = config.isWindows; 26 | } 27 | 28 | /** 29 | * 检查路径是否存在 30 | */ 31 | router["GET /file/path/exist"] = async function (ctx: Context) { 32 | ctx.body = await FileService.checkExist(ctx.query.path as string); 33 | }; 34 | 35 | /** 36 | * 收藏路径 37 | */ 38 | router["POST /file/path/save"] = async function (ctx: Context) { 39 | ctx.body = await FileService.savePath(ctx.request.body); 40 | }; 41 | 42 | /** 43 | * 获取收藏路径 44 | */ 45 | router["GET /file/path"] = async function (ctx: Context) { 46 | ctx.body = await FileService.getSaveList(); 47 | }; 48 | 49 | /** 50 | * 获取收藏路径 51 | */ 52 | router["DELETE /file/path/delete"] = async function (ctx: Context) { 53 | ctx.body = await FileService.deleteOne(ctx.query.id); 54 | }; 55 | 56 | /** 57 | * delete file batch 58 | */ 59 | router["POST /file/deleteBatch"] = async function (ctx: Context) { 60 | ctx.body = await FileService.deleteBatch(ctx.request.body); 61 | }; 62 | 63 | /** 64 | * rename file 65 | */ 66 | router["POST /file/rename"] = async function (ctx: Context) { 67 | await FileService.rename(ctx.request.body.source, ctx.request.body.target); 68 | ctx.body = ""; 69 | }; 70 | 71 | export default router; 72 | -------------------------------------------------------------------------------- /openRenamerBackend/api/GlobalConfigApi.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "koa"; 2 | import service from "../service/GlobalConfigService"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 获取单个配置 8 | */ 9 | router["GET /config/code"] = async function (ctx: Context) { 10 | ctx.body = await service.getVal(ctx.request.query.code as string); 11 | }; 12 | 13 | /** 14 | * 获取多个配置项 15 | */ 16 | router["POST /config/multCode"] = async function (ctx: Context) { 17 | ctx.body = await service.getMultVal(ctx.request.body); 18 | }; 19 | 20 | /** 21 | * 提交修改 22 | */ 23 | router["POST /config/update"] = async function (ctx: Context) { 24 | ctx.body = await service.updateVal(ctx.request.body.code, ctx.request.body.val); 25 | }; 26 | 27 | /** 28 | * 提交修改 29 | */ 30 | router["POST /config/insertOrUpdate"] = async function (ctx: Context) { 31 | ctx.body = await service.insertOrReplace(ctx.request.body); 32 | }; 33 | 34 | 35 | 36 | export default router; 37 | -------------------------------------------------------------------------------- /openRenamerBackend/api/PublicApi.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "koa"; 2 | import config from "../config"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 判断token是否正确 8 | */ 9 | router["POST /public/checkToken"] = async function (ctx: Context) { 10 | ctx.body = ctx.request.body.token === config.token; 11 | }; 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /openRenamerBackend/api/QbServiceApi.ts: -------------------------------------------------------------------------------- 1 | import {Context} from "koa"; 2 | import service from "../service/QbService"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 获取单个配置 8 | */ 9 | router["POST /qb/saveQbInfo"] = async function (ctx: Context) { 10 | ctx.body = await service.saveAddress(ctx.request.body); 11 | }; 12 | 13 | /** 14 | * 获取qb配置 15 | */ 16 | router["GET /qb/config"] = async function (ctx: Context) { 17 | ctx.body = await service.getConfig(); 18 | }; 19 | 20 | /** 21 | * 获取qb配置 22 | */ 23 | router["GET /qb/bt/list"] = async function (ctx: Context) { 24 | ctx.body = await service.getBtList(); 25 | }; 26 | 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /openRenamerBackend/api/RenamerApi.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "koa"; 2 | import RenamerService from "../service/RenamerService"; 3 | 4 | const router = {}; 5 | 6 | /** 7 | * 预览文件修改后的状态 8 | */ 9 | router["POST /renamer/preview"] = async function (ctx: Context) { 10 | ctx.body = await RenamerService.preview(ctx.request.body.fileList, ctx.request.body.ruleList); 11 | }; 12 | 13 | /** 14 | * 提交修改 15 | */ 16 | router["POST /renamer/submit"] = async function (ctx: Context) { 17 | ctx.body = await RenamerService.rename(ctx.request.body.fileList, ctx.request.body.changedFileList); 18 | }; 19 | 20 | 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /openRenamerBackend/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as process from "process"; 3 | 4 | //后台所在绝对路径 5 | const rootPath = path.resolve(__dirname, '..'); 6 | 7 | let config = { 8 | rootPath, 9 | dataPath: process.env.DATA_PATH ? process.env.DATA_PATH : path.join(rootPath, 'data'), 10 | port: process.env.PORT ? parseInt(process.env.PORT) : 8089, 11 | token: process.env.TOKEN ? process.env.TOKEN : null, 12 | urlPrefix: '/openRenamer/api', 13 | //是否为windows平台 14 | isWindows: process.platform.toLocaleLowerCase().includes("win"), 15 | bodyLimit: { 16 | formLimit: '200mb', 17 | jsonLimit: '200mb', 18 | urlencoded: true, 19 | multipart: true, 20 | formidable: { 21 | uploadDir: path.join(rootPath, 'files', 'temp', 'uploads'), 22 | keepExtenstions: true, 23 | maxFieldsSize: 1024 * 1024 * 200 24 | } 25 | }, 26 | publicPath: new Set(["POST/public/checkToken"]) 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /openRenamerBackend/dao/ApplicationRuleDao.ts: -------------------------------------------------------------------------------- 1 | import ErrorHelper from "../util/ErrorHelper"; 2 | import ApplicationRule from "../entity/po/ApplicationRule"; 3 | import SqliteHelper from "../util/SqliteHelper"; 4 | 5 | export default class ApplicationRuleDao { 6 | /** 7 | * 查询所有 8 | * @param obj 9 | * @returns 10 | */ 11 | static async getAll(): Promise> { 12 | let res = await SqliteHelper.pool.all('select id,createdDate,updatedDate,name,comment,content from application_rule'); 13 | return res; 14 | } 15 | 16 | /** 17 | * 查询id 18 | * @param id id 19 | * @returns 20 | */ 21 | static async getById(id: number): Promise { 22 | let res = await SqliteHelper.pool.get('select * from application_rule where id=?', id); 23 | return res; 24 | } 25 | 26 | 27 | 28 | 29 | /** 30 | * 新增 31 | * @param obj 32 | * @returns 33 | */ 34 | static async addOne(obj: ApplicationRule): Promise { 35 | let res = await SqliteHelper.pool.run('insert into application_rule(createdDate,updatedDate,name,comment,content) values(?,?,?,?,?)' 36 | , obj.createdDate, obj.updatedDate, obj.name, obj.comment, obj.content); 37 | return res.lastID; 38 | } 39 | 40 | /** 41 | * 更新 42 | * @param obj 43 | */ 44 | static async updateOne(obj: ApplicationRule): Promise { 45 | let res = await SqliteHelper.pool.run('update application_rule set updatedDate=?,name=?,comment=?,content=? where id=?' 46 | , obj.updatedDate, obj.name, obj.comment, obj.content, obj.id); 47 | if (res.changes == 0) { 48 | throw ErrorHelper.Error404("数据不存在"); 49 | } 50 | } 51 | 52 | /** 53 | * 删除 54 | * @param id 55 | */ 56 | static async delete(id: number): Promise { 57 | let res = await SqliteHelper.pool.run('delete from application_rule where id=?', id); 58 | if (res.changes == 0) { 59 | throw ErrorHelper.Error404("数据不存在"); 60 | } 61 | } 62 | 63 | 64 | } -------------------------------------------------------------------------------- /openRenamerBackend/dao/GlobalConfigDao.ts: -------------------------------------------------------------------------------- 1 | import ErrorHelper from "../util/ErrorHelper"; 2 | import SqliteHelper from "../util/SqliteHelper"; 3 | import GlobalConfig from "../entity/po/GlobalConfig"; 4 | 5 | export default class GlobalConfigDao { 6 | 7 | 8 | /** 9 | * 新增 10 | * @param obj 11 | * @returns 12 | */ 13 | static async addOne(obj: GlobalConfig): Promise { 14 | await SqliteHelper.pool.run('insert into global_config(code,val,description) values(?,?,?)' 15 | , obj.code, obj.val, obj.description); 16 | } 17 | 18 | /** 19 | * 更新 20 | * @param code code 21 | * @param val val 22 | */ 23 | static async updateOne(code: string, val: string): Promise { 24 | await SqliteHelper.pool.run('update global_config set val=? where code=?', val, code); 25 | } 26 | 27 | 28 | /** 29 | * 删除 30 | * @param code 31 | */ 32 | static async deleteByCode(code: string): Promise { 33 | let res = await SqliteHelper.pool.run('delete from global_config where code=?', code); 34 | if (res.changes == 0) { 35 | throw ErrorHelper.Error404("数据不存在"); 36 | } 37 | } 38 | 39 | /** 40 | * 查询 41 | * @param code 42 | */ 43 | static async getByCode(code: string): Promise { 44 | let res = await SqliteHelper.pool.get('select val from global_config where code=?', code); 45 | return res ? res.val : null; 46 | } 47 | 48 | /** 49 | * 查询多个code 50 | * @param code 51 | */ 52 | static async getByMulCode(codes: Array): Promise> { 53 | if (codes.length == 0) { 54 | return new Array(); 55 | } 56 | let codeStr = codes.map(item => `'${item}'`).join(','); 57 | return await SqliteHelper.pool.all(`select * from global_config where code in (${codeStr})`); 58 | } 59 | 60 | /** 61 | * 插入一条 62 | * @param body body 63 | */ 64 | static async insertOrReplace(body: GlobalConfig): Promise { 65 | await SqliteHelper.pool.run(`insert or replace into global_config values (?,?,?)`, body.code, body.val, body.description); 66 | } 67 | 68 | 69 | } -------------------------------------------------------------------------------- /openRenamerBackend/dao/SavePathDao.ts: -------------------------------------------------------------------------------- 1 | import ErrorHelper from "../util/ErrorHelper"; 2 | import SavePath from "../entity/po/SavePath"; 3 | import SqliteHelper from "../util/SqliteHelper"; 4 | 5 | export default class SavePathDao { 6 | /** 7 | * 查询所有 8 | * @param obj 9 | * @returns 10 | */ 11 | static async getAll(): Promise> { 12 | let res = await SqliteHelper.pool.all('select id,name,content from path_save'); 13 | return res; 14 | } 15 | 16 | 17 | /** 18 | * 新增 19 | * @param obj 20 | * @returns 21 | */ 22 | static async addOne(obj: SavePath): Promise { 23 | let res = await SqliteHelper.pool.run('insert into path_save(name,content) values(?,?)' 24 | , obj.name, obj.content); 25 | obj.id = res.lastID; 26 | return res.lastID; 27 | } 28 | 29 | 30 | /** 31 | * 删除 32 | * @param id 33 | */ 34 | static async delete(id: number): Promise { 35 | let res = await SqliteHelper.pool.run('delete from path_save where id=?', id); 36 | if (res.changes == 0) { 37 | throw ErrorHelper.Error404("数据不存在"); 38 | } 39 | } 40 | 41 | 42 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/AutoRule.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import FileObj from "../../vo/FileObj"; 3 | import path from 'path'; 4 | import {getSeason} from "../../../util/MediaUtil"; 5 | 6 | 7 | let pattern = new RegExp(/s(eason)?(\d+)/); 8 | let eNumPatternArr = [new RegExp(/ep?(\d+)/), new RegExp(/[\(\[(](\d+)[\)\])]/), new RegExp(/[\.-](\d+)/), new RegExp(/(\d+)/)]; 9 | let resolutionPattern = new RegExp(/(\d{3,}[pP])/); 10 | let resolutionArr = ['1k', '1K', '2k', '2K', '4k', '4K', '8k', '8K']; 11 | let charSet = new Set([' ', '[', '.', '(', '(']); 12 | export default class InsertRule implements RuleInterface { 13 | 14 | /** 15 | * 识别类型,season:季号,name:剧名/电影名识别 16 | */ 17 | type: string; 18 | /** 19 | * 前面追加 20 | */ 21 | frontAdd: string; 22 | /** 23 | * 后面追加 24 | */ 25 | endAdd: string; 26 | eNumWidth: number; 27 | 28 | constructor(data: any) { 29 | this.type = data.type; 30 | this.frontAdd = data.frontAdd; 31 | this.endAdd = data.endAdd; 32 | this.eNumWidth = data.eNumWidth; 33 | } 34 | 35 | 36 | deal(file: FileObj): void { 37 | //识别到的内容 38 | let getStr = null; 39 | let season = getSeason(path.basename(file.path)); 40 | if (this.type === 'season') { 41 | getStr = season; 42 | } else if (this.type === 'name') { 43 | let originName = null; 44 | if (season && season.length > 0) { 45 | //说明是剧集,取父文件夹的父文件夹名称 46 | originName = path.basename(path.resolve(file.path, '..')); 47 | } else { 48 | //说明是电影 49 | originName = path.basename(file.path); 50 | } 51 | getStr = ''; 52 | for (let i = 0; i < originName.length; i++) { 53 | let char = originName.charAt(i); 54 | if (charSet.has(char)) { 55 | break; 56 | } 57 | getStr += char; 58 | } 59 | } else if (this.type === 'eNum') { 60 | let lowName = file.originName.toLocaleLowerCase().replace(/ /g, '') 61 | .replace(/\d+[kp]/g, '')//去除4k,1080p等 62 | .replace(/[xh]\d+/g, '')//去除x264,h264等 ; 63 | for (let i in eNumPatternArr) { 64 | let patternRes = lowName.match(eNumPatternArr[i]); 65 | if (patternRes && patternRes.length > 1) { 66 | getStr = patternRes[1]; 67 | for (let i = 0; i < this.eNumWidth - getStr.length; i++) { 68 | getStr = '0' + getStr; 69 | } 70 | break; 71 | } 72 | } 73 | } else if (this.type === 'resolution') { 74 | let res = file.originName.match(resolutionPattern); 75 | if (res && res.length > 1) { 76 | getStr = res[1]; 77 | } else { 78 | for (let i = 0; i < resolutionArr.length; i++) { 79 | if (file.originName.indexOf(resolutionArr[i]) > -1) { 80 | getStr = resolutionArr[i]; 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | if (getStr && getStr.length > 0) { 87 | file.realName = file.realName + this.frontAdd + getStr + this.endAdd; 88 | file.name = file.realName + file.expandName; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/DeleteRule.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import {dealFileName} from "./RuleInterface"; 3 | import FileObj from "../../vo/FileObj"; 4 | import path from 'path'; 5 | 6 | export default class DeleteRule implements RuleInterface { 7 | /** 8 | * 类别:deletePart:部分删除,deleteAll:全部删除 9 | */ 10 | type: string; 11 | /** 12 | * 部分删除时的开始信息 13 | */ 14 | start: DeleteRuleItem; 15 | /** 16 | * 部分删除时的结束信息 17 | 18 | */ 19 | end: DeleteRuleItem; 20 | /** 21 | * 忽略拓展名,true:忽略,false:不忽略 22 | */ 23 | ignorePostfix: boolean; 24 | /* 25 | * 是否区分大小写 26 | */ 27 | regI: boolean; 28 | 29 | constructor(data: any) { 30 | this.type = data.type; 31 | this.regI = data.regI != undefined && data.regI; 32 | this.start = new DeleteRuleItem(data.start, this.regI); 33 | this.end = new DeleteRuleItem(data.end, this.regI); 34 | this.ignorePostfix = data.ignorePostfix; 35 | } 36 | 37 | 38 | deal(file: FileObj): void { 39 | let target = ""; 40 | if (this.type === 'deleteAll') { 41 | target = ""; 42 | } else { 43 | let str = file.realName + (this.ignorePostfix ? "" : file.expandName); 44 | let startIndex = this.start.calIndex(str, false); 45 | let endIndex = this.end.calIndex(str, true); 46 | if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) { 47 | return; 48 | } 49 | str = str.substring(0, startIndex) + str.substring(endIndex + 1); 50 | target = str; 51 | } 52 | dealFileName(file, target, this.ignorePostfix); 53 | } 54 | 55 | } 56 | 57 | class DeleteRuleItem { 58 | /** 59 | * location:位置,text:文本,end:直到末尾 60 | */ 61 | type: string; 62 | /** 63 | * 对应的值 64 | */ 65 | value: string; 66 | /** 67 | * 正则对象 68 | */ 69 | reg: RegExp; 70 | 71 | constructor(data: any, regI: boolean) { 72 | this.type = data.type; 73 | this.value = data.value; 74 | if (this.type === 'reg') { 75 | this.reg = regI ? new RegExp(this.value) : new RegExp(this.value, 'i'); 76 | } 77 | } 78 | 79 | /** 80 | * 计算位置 81 | * @param str 字符串 82 | * @param end 是否末尾计算 83 | */ 84 | calIndex(str: string, end: boolean): number { 85 | if (this.type === 'location') { 86 | let val = parseInt(this.value); 87 | return val > 0 ? val - 1 : str.length + val; 88 | } else if (this.type === 'text') { 89 | let index = str.indexOf(this.value); 90 | return index + (end ? this.value.length - 1 : 0); 91 | } else if (this.type === 'end') { 92 | return str.length - 1; 93 | } else if (this.type === 'reg') { 94 | let res = this.reg.exec(str); 95 | return res == null ? -1 : (res.index + (end ? res[0].length - 1 : 0)); 96 | } 97 | return -1; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/InsertRule.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import FileObj from "../../vo/FileObj"; 3 | import path from 'path'; 4 | import {getSeason} from "../../../util/MediaUtil"; 5 | 6 | 7 | export default class InsertRule implements RuleInterface { 8 | 9 | /** 10 | * 插入内容 11 | */ 12 | insertContent: string; 13 | /** 14 | * 操作类别,front:前缀,backend:后缀,at:位置,replace:替换当前文件名 15 | */ 16 | type: string; 17 | /** 18 | * 当type为at,时的位置,从1开始 19 | */ 20 | atInput: number; 21 | /** 22 | * 当type为at,时的方向,true:从右到左,false:从左到右 23 | */ 24 | atIsRightToleft: boolean; 25 | /** 26 | * 忽略拓展名,true:忽略,false:不忽略 27 | */ 28 | ignorePostfix: boolean; 29 | /** 30 | 自动识别季号 31 | */ 32 | autoSeason: boolean; 33 | /** 34 | 后缀过滤是否开启 35 | */ 36 | endFilter: boolean; 37 | /** 38 | 有效后缀 39 | */ 40 | validEnd: Array; 41 | 42 | constructor(data: any) { 43 | this.insertContent = data.insertContent; 44 | this.type = data.type; 45 | this.atInput = data.atInput; 46 | this.atIsRightToleft = data.atIsRightToleft; 47 | this.ignorePostfix = data.ignorePostfix; 48 | this.autoSeason = data.autoSeason; 49 | this.endFilter = data.endFilter; 50 | this.validEnd = data.validEnd; 51 | } 52 | 53 | 54 | deal(file: FileObj): void { 55 | if (this.endFilter && file.expandName.length > 0 && this.validEnd.indexOf(file.expandName.substring(1)) == -1) { 56 | //拓展名不符,跳过 57 | return; 58 | } 59 | let str = this.ignorePostfix ? file.realName : file.name; 60 | let season = ''; 61 | 62 | if (this.autoSeason) { 63 | season = getSeason(path.basename(file.path)); 64 | } 65 | switch (this.type) { 66 | case "front": 67 | str = this.insertContent + season + str; 68 | break; 69 | case "backend": 70 | str = str + this.insertContent + season; 71 | break; 72 | case "at": 73 | let index = this.atIsRightToleft ? str.length - this.atInput + 1 : this.atInput - 1; 74 | str = str.substring(0, index) + this.insertContent + season + str.substring(index); 75 | break; 76 | case "replace": 77 | str = this.insertContent + season; 78 | break; 79 | } 80 | 81 | 82 | if (this.ignorePostfix) { 83 | file.realName = str; 84 | } else { 85 | file.expandName = path.extname(str); 86 | if (file.expandName.length > 0) { 87 | file.realName = str.substring(0, str.lastIndexOf(".")); 88 | } else { 89 | file.realName = str; 90 | } 91 | } 92 | 93 | file.name = file.realName + file.expandName; 94 | } 95 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/ReplaceRule.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import * as ValUtil from "../../../util/ValUtil"; 3 | import FileObj from "../../vo/FileObj"; 4 | import {dealFileName} from './RuleInterface'; 5 | import path from 'path'; 6 | 7 | 8 | export default class ReplaceRule implements RuleInterface { 9 | 10 | /** 11 | * 1:替换第一个,2:替换最后一个,3:全部替换 12 | */ 13 | type: number; 14 | /** 15 | * 源 16 | */ 17 | source: string; 18 | /** 19 | * 目标 20 | */ 21 | target: string; 22 | /** 23 | * 是否正则模式 24 | */ 25 | regFlag: boolean; 26 | /** 27 | * 是否区分大小写 28 | */ 29 | regI: boolean; 30 | /** 31 | * 是否护理拓展名 32 | */ 33 | ignorePostfix: boolean; 34 | 35 | constructor(data: any) { 36 | this.type = data.type; 37 | this.source = data.source; 38 | this.target = data.target; 39 | this.regFlag = ValUtil.nullToDefault(data.regFlag, false); 40 | this.regI = ValUtil.nullToDefault(data.regI, false); 41 | this.ignorePostfix = ValUtil.nullToDefault(data.ignorePostfix, false); 42 | } 43 | 44 | 45 | deal(file: FileObj): void { 46 | let targetStr = this.ignorePostfix ? file.realName : file.name; 47 | let res = this.regFlag ? this.dealReg(targetStr) : this.dealNoReg(targetStr); 48 | dealFileName(file, res, this.ignorePostfix); 49 | } 50 | 51 | private dealNoReg(targetStr: string): string { 52 | let start = 0; 53 | let arr: number[] = []; 54 | for (let i = 0; i < (this.type == 1 ? 1 : 1000); i++) { 55 | let one = targetStr.indexOf(this.source, start); 56 | if (one == -1) { 57 | break; 58 | } 59 | arr.push(one); 60 | start = one + this.source.length; 61 | } 62 | if (arr.length == 0) { 63 | return targetStr; 64 | } 65 | let res = ""; 66 | let needDealArr: number[] = this.type === 1 ? [arr[0]] : this.type === 2 ? [arr[arr.length - 1]] : arr; 67 | let lastIndex = 0; 68 | for (let i = 0; i < needDealArr.length; i++) { 69 | res += targetStr.substring(lastIndex, needDealArr[i]) + this.target; 70 | lastIndex = needDealArr[i] + this.source.length; 71 | } 72 | res += targetStr.substring(lastIndex); 73 | return res; 74 | } 75 | 76 | private dealReg(targetStr: string): string { 77 | let templateReg = new RegExp("#\{group(\\d+\)}", "g"); 78 | let templateArr: string[][] = []; 79 | while (true) { 80 | let one = templateReg.exec(this.target); 81 | if (one == null) { 82 | break; 83 | } 84 | templateArr.push([one[0], one[1]]); 85 | } 86 | 87 | let reg = new RegExp(this.source, this.regI ? "g" : "ig"); 88 | let arr: RegExpExecArray[] = []; 89 | for (let i = 0; i < (this.type == 1 ? 1 : 1000); i++) { 90 | let one = reg.exec(targetStr); 91 | if (one == null) { 92 | break; 93 | } 94 | arr.push(one); 95 | } 96 | if (arr.length == 0) { 97 | return targetStr; 98 | } 99 | let res = ""; 100 | let needDealReg: RegExpExecArray[] = this.type === 1 ? [arr[0]] : this.type === 2 ? [arr[arr.length - 1]] : arr; 101 | let lastIndex = 0; 102 | for (let i = 0; i < needDealReg.length; i++) { 103 | let reg = needDealReg[i]; 104 | let target = this.target; 105 | templateArr.forEach(item => target = target.replace(item[0], ValUtil.nullToDefault(reg[parseInt(item[1])], ''))); 106 | res += targetStr.substring(lastIndex, reg.index) + target; 107 | lastIndex = reg.index + reg[0].length; 108 | } 109 | res += targetStr.substring(lastIndex); 110 | return res; 111 | } 112 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/RuleInterface.ts: -------------------------------------------------------------------------------- 1 | import FileObj from "../../vo/FileObj"; 2 | import * as path from 'path'; 3 | 4 | export default interface RuleInterface { 5 | 6 | deal(file: FileObj): void; 7 | } 8 | 9 | /** 10 | * 重新处理文件名 11 | * @param file 12 | * @param newFileName 13 | * @param ignorePostfix 14 | */ 15 | export function dealFileName(file: FileObj, newFileName: string, ignorePostfix: boolean) { 16 | if (ignorePostfix) { 17 | file.realName = newFileName; 18 | } else { 19 | file.expandName = path.extname(newFileName); 20 | if (file.expandName.length > 0) { 21 | file.realName = newFileName.substring(0, newFileName.lastIndexOf(".")); 22 | } else { 23 | file.realName = newFileName; 24 | } 25 | } 26 | file.name = file.realName + file.expandName; 27 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/SerializationRule.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import FileObj from "../../vo/FileObj"; 3 | import path from 'path'; 4 | 5 | export default class InsertRule implements RuleInterface { 6 | /** 7 | * 开始位置 8 | */ 9 | start: number; 10 | /** 11 | * 记录当前的值是多少 12 | */ 13 | currentIndexMap: Map; 14 | /** 15 | * 增量 16 | */ 17 | increment: number; 18 | /** 19 | * 是否填充0 20 | */ 21 | addZero: boolean; 22 | /** 23 | * 填充后长度 24 | */ 25 | numLength: number; 26 | /** 27 | * 插入位置,front:前缀,backend:后缀,at:位置 28 | */ 29 | insertType: string; 30 | /** 31 | * 插入的位置 32 | */ 33 | insertValue: number; 34 | /** 35 | * 忽略拓展名 36 | */ 37 | ignorePostfix: boolean; 38 | /** 39 | * 拓展名分组 40 | */ 41 | postfixGroup: boolean; 42 | 43 | constructor(data: any) { 44 | this.start = data.start; 45 | this.currentIndexMap = new Map(); 46 | this.increment = data.increment; 47 | this.addZero = data.addZero; 48 | this.numLength = data.numLength; 49 | this.insertType = data.insertType; 50 | this.insertValue = data.insertValue; 51 | this.ignorePostfix = data.ignorePostfix; 52 | this.postfixGroup = data.postfixGroup; 53 | } 54 | 55 | deal(file: FileObj): void { 56 | let expand = this.postfixGroup ? file.expandName : ""; 57 | let currentIndex = this.currentIndexMap.has(expand) ? this.currentIndexMap.get(expand) : this.start; 58 | let length = currentIndex.toString().length; 59 | let numStr = (this.addZero && this.numLength > length ? "0".repeat(this.numLength - length) : "") + currentIndex; 60 | let str = this.ignorePostfix ? file.realName : file.name; 61 | switch (this.insertType) { 62 | case "front": 63 | str = numStr + str; 64 | break; 65 | case "backend": 66 | str = str + numStr; 67 | break; 68 | case "at": 69 | str = str.substring(0, this.insertValue - 1) + numStr + str.substring(this.insertValue - 1); 70 | break; 71 | } 72 | this.currentIndexMap.set(expand, currentIndex + this.increment); 73 | 74 | if (this.ignorePostfix) { 75 | file.realName = str; 76 | } else { 77 | file.expandName = path.extname(str); 78 | if (file.expandName.length > 0) { 79 | file.realName = str.substring(0, str.lastIndexOf(".")); 80 | } else { 81 | file.realName = str; 82 | } 83 | } 84 | 85 | file.name = file.realName + file.expandName; 86 | } 87 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/bo/rules/TranslateRole.ts: -------------------------------------------------------------------------------- 1 | import RuleInterface from "./RuleInterface"; 2 | import FileObj from "../../vo/FileObj"; 3 | import * as TranslateUtil from "../../../util/TranslateUtil"; 4 | import path from 'path'; 5 | 6 | 7 | export default class TranslateRole implements RuleInterface { 8 | 9 | /** 10 | * 1:简体转繁体 2:繁体转简体 11 | */ 12 | type: number; 13 | /** 14 | * 0、繁体中文,1、港澳繁体,2、台湾正体 15 | */ 16 | traditionalType: number; 17 | 18 | constructor(data: any) { 19 | this.type = data.type; 20 | this.traditionalType = data.traditionalType; 21 | } 22 | 23 | 24 | deal(file: FileObj): void { 25 | if (this.type == 1) { 26 | file.realName = TranslateUtil.toTraditionalChinese(file.realName, this.traditionalType); 27 | } else if (this.type == 2) { 28 | file.realName = TranslateUtil.toSimplifiedChinese(file.realName, this.traditionalType); 29 | } 30 | file.name = file.realName + file.expandName; 31 | } 32 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/constants/GlobalConfigCodeConstant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 默认模板id 3 | */ 4 | export const DEFAULT_TEMPLETE_ID = "defaultTempleteId"; -------------------------------------------------------------------------------- /openRenamerBackend/entity/dto/AutoPlanConfigDto.ts: -------------------------------------------------------------------------------- 1 | export default interface AutoPlanConfigDto { 2 | /** 3 | * 待处理的路径 4 | */ 5 | paths: Array; 6 | /** 7 | * 版本 8 | */ 9 | version: Number; 10 | /** 11 | * 是否忽略season 0 12 | */ 13 | ignoreSeason0: Boolean; 14 | /** 15 | * 忽略的文件名 16 | */ 17 | ignorePaths: Array; 18 | /** 19 | * 是否删除小于2m的视频文件 20 | */ 21 | deleteSmallVideo: boolean; 22 | /** 23 | * 重命名规则 24 | */ 25 | rules: Array; 26 | /** 27 | * 是否忽略现有的文件 28 | */ 29 | ignoreExist: boolean; 30 | /** 31 | * 是否开始任务 32 | */ 33 | start: boolean; 34 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/dto/BtListItemDto.ts: -------------------------------------------------------------------------------- 1 | export default interface BtListItemDto { 2 | hash: string; 3 | /** 4 | * 添加时间 5 | */ 6 | added_on: number; 7 | /** 8 | * left bytes num 9 | */ 10 | amount_left: number; 11 | /** 12 | * Percentage of file pieces currently available 13 | */ 14 | availability: number; 15 | category: string; 16 | /** 17 | * Amount of transfer data completed (bytes) 18 | */ 19 | completed: number; 20 | /** 21 | * Time (Unix Epoch) when the torrent completed 22 | */ 23 | completion_on: number; 24 | /** 25 | * Absolute path of torrent content (root path for multifile torrents, absolute file path for singlefile torrents) 26 | */ 27 | content_path: string; 28 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/dto/QbConfigDto.ts: -------------------------------------------------------------------------------- 1 | export default interface QbConfigDto { 2 | address: string; 3 | username: string; 4 | password: string; 5 | valid: boolean; 6 | /** 7 | * qb version,null if config is error 8 | */ 9 | version: string; 10 | /** 11 | * Qbittorrent's download 12 | */ 13 | qbDownloadPath: string; 14 | /** 15 | * Qbittorrent's download path corresponds to current system path 16 | */ 17 | renameQbDownloadPath: string; 18 | /** 19 | * config path to select convenient 20 | */ 21 | configPaths: Array; 22 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/po/ApplicationRule.ts: -------------------------------------------------------------------------------- 1 | export default class ApplicationRule { 2 | /** 3 | 创建时间 4 | */ 5 | createdDate: number; 6 | /** 7 | 更新时间 8 | */ 9 | updatedDate: number; 10 | id: number; 11 | /** 12 | 名称 13 | */ 14 | name: string; 15 | /** 16 | 说明 17 | */ 18 | comment: string; 19 | /** 20 | 规则内容,json序列化后 21 | */ 22 | content: string; 23 | 24 | constructor(name: string, comment: string, content: string) { 25 | this.createdDate = Date.now(); 26 | this.updatedDate = this.createdDate; 27 | this.name = name; 28 | this.comment = comment; 29 | this.content = content; 30 | } 31 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/po/GlobalConfig.ts: -------------------------------------------------------------------------------- 1 | export default class GlobalConfig { 2 | /** 3 | code 4 | */ 5 | code: string; 6 | 7 | /** 8 | 规则内容,json序列化后 9 | */ 10 | val: string; 11 | /** 12 | 描述 13 | */ 14 | description: string; 15 | 16 | constructor(code: string, val: string, desc: string) { 17 | this.code = code; 18 | this.val = val; 19 | this.description = desc; 20 | } 21 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/po/SavePath.ts: -------------------------------------------------------------------------------- 1 | export default class SavePath { 2 | id: number; 3 | /** 4 | 名称 5 | */ 6 | name: string; 7 | 8 | /** 9 | 规则内容,json序列化后 10 | */ 11 | content: string; 12 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/vo/FileObj.ts: -------------------------------------------------------------------------------- 1 | import * as pathUtil from "path"; 2 | import {isVideo, isSub, isNfo} from "../../util/MediaUtil" 3 | 4 | export default class FileObj { 5 | /** 6 | * 变更后的文件名(包含拓展名) 7 | */ 8 | name: string; 9 | /** 10 | * 去掉拓展名后的名字(不包含拓展名) 11 | */ 12 | realName: string; 13 | /** 14 | 原始文件名(不变) 15 | */ 16 | originName: string; 17 | /** 18 | * 拓展名(最新的拓展名,每次应用规则后重新计算) 19 | */ 20 | expandName: string; 21 | 22 | /** 23 | * 所属路径 24 | */ 25 | path: string; 26 | /** 27 | * 是否文件夹 28 | */ 29 | isFolder: boolean; 30 | /** 31 | * 文件大小 32 | */ 33 | size: number; 34 | /** 35 | * 重命名错误原因 36 | */ 37 | errorMessage: string; 38 | /** 39 | * 创建时间ms 40 | */ 41 | createdTime: number; 42 | /** 43 | * 更新时间ms 44 | */ 45 | updatedTime: number; 46 | /** 47 | * 是否广告文件 48 | */ 49 | isAdFile: boolean; 50 | 51 | 52 | constructor(name: string, path, isFolder, size: number, createdTime, updatedTime) { 53 | this.name = name; 54 | this.originName = name; 55 | this.expandName = pathUtil.extname(name); 56 | if (this.expandName.length > 0) { 57 | this.realName = name.substring(0, name.lastIndexOf(".")); 58 | let end = this.expandName.toLowerCase().replace(".", ""); 59 | if (isVideo(end)) { 60 | this.isAdFile = size < 5 * 1024 * 1024; 61 | } else this.isAdFile = !(isSub(end) || isNfo(end)); 62 | } else { 63 | this.realName = name; 64 | } 65 | this.path = path; 66 | this.isFolder = isFolder; 67 | this.size = size; 68 | this.createdTime = createdTime; 69 | this.updatedTime = updatedTime; 70 | 71 | } 72 | } -------------------------------------------------------------------------------- /openRenamerBackend/entity/vo/RuleObj.ts: -------------------------------------------------------------------------------- 1 | import DeleteRule from "../bo/rules/DeleteRule"; 2 | import InsertRule from "../bo/rules/InsertRule"; 3 | import SerializationRule from "../bo/rules/SerializationRule"; 4 | import AutoRule from "../bo/rules/AutoRule"; 5 | import ReplaceRule from "../bo/rules/ReplaceRule"; 6 | import TranslateRole from "../bo/rules/TranslateRole"; 7 | 8 | export default class RuleObj { 9 | type: string; 10 | message: string; 11 | /** 12 | * 具体参数 13 | */ 14 | data: any; 15 | 16 | constructor(data: any) { 17 | this.type = data.type; 18 | this.message = data.message; 19 | switch (this.type) { 20 | case "delete": 21 | this.data = new DeleteRule(data.data); 22 | break; 23 | case "insert": 24 | this.data = new InsertRule(data.data); 25 | break; 26 | case "serialization": 27 | this.data = new SerializationRule(data.data); 28 | break; 29 | case "auto": 30 | this.data = new AutoRule(data.data); 31 | break; 32 | case "replace": 33 | this.data = new ReplaceRule(data.data); 34 | break; 35 | case "translate": 36 | this.data = new TranslateRole(data.data); 37 | break; 38 | default: 39 | throw new Error("不支持的规则:" + this.type); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /openRenamerBackend/index.ts: -------------------------------------------------------------------------------- 1 | import koa from "koa"; 2 | import Router from "koa-router"; 3 | import koaBody from "koa-body"; 4 | import * as path from "path"; 5 | import RouterMW from "./middleware/controllerEngine"; 6 | 7 | import config from "./config"; 8 | import handleError from "./middleware/handleError"; 9 | import init from "./middleware/init"; 10 | import SqliteUtil from './util/SqliteHelper'; 11 | import log from './util/LogUtil'; 12 | import QbService from './service/QbService'; 13 | import qbService from "./service/QbService"; 14 | 15 | console.log(config); 16 | 17 | const app = new koa(); 18 | 19 | let router = new Router({ 20 | prefix: config.urlPrefix 21 | }); 22 | 23 | app.use(require('koa-static')(path.join(config.rootPath, 'static'))); 24 | 25 | //表单解析 26 | app.use(koaBody(config.bodyLimit)); 27 | //请求预处理 28 | app.use(init); 29 | //错误处理 30 | app.use(handleError); 31 | 32 | app.use(RouterMW(router, path.join(config.rootPath, "dist/api"))); 33 | (async () => { 34 | await SqliteUtil.createPool(); 35 | await qbService.init(); 36 | app.listen(config.port); 37 | log.info(`server listened `, config.port); 38 | })(); 39 | 40 | app.on("error", (error) => { 41 | console.error(error); 42 | }) 43 | -------------------------------------------------------------------------------- /openRenamerBackend/middleware/controllerEngine.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import * as path from 'path'; 3 | import log from '../util/LogUtil'; 4 | 5 | async function addMapping(router, filePath: string) { 6 | let mapping = require(filePath).default; 7 | for (let url in mapping) { 8 | if (url.startsWith('GET ')) { 9 | let temp = url.substring(4); 10 | router.get(temp, mapping[url]); 11 | log.info(`----GET:${temp}`); 12 | } else if (url.startsWith('POST ')) { 13 | let temp = url.substring(5); 14 | router.post(temp, mapping[url]); 15 | log.info(`----POST:${temp}`); 16 | } else if (url.startsWith('PUT ')) { 17 | let temp = url.substring(4); 18 | router.put(temp, mapping[url]); 19 | log.info(`----PUT:${temp}`); 20 | } else if (url.startsWith('DELETE ')) { 21 | let temp = url.substring(7); 22 | router.delete(temp, mapping[url]); 23 | log.info(`----DELETE: ${temp}`); 24 | } else { 25 | log.info(`xxxxx无效路径:${url}`); 26 | } 27 | } 28 | } 29 | 30 | function addControllers(router, filePath) { 31 | let files = fs.readdirSync(filePath).filter(item => item.endsWith('.js')); 32 | for (let index in files) { 33 | let element = files[index]; 34 | let temp = path.join(filePath, element); 35 | let state = fs.statSync(temp); 36 | if (state.isDirectory()) { 37 | addControllers(router, temp); 38 | } else { 39 | if (!temp.endsWith('Helper.js')) { 40 | log.info('\n--开始处理: ' + element + '路由'); 41 | addMapping(router, temp); 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default function engine(router, folder) { 48 | addControllers(router, folder); 49 | return router.routes(); 50 | } 51 | -------------------------------------------------------------------------------- /openRenamerBackend/middleware/handleError.ts: -------------------------------------------------------------------------------- 1 | import log from '../util/LogUtil'; 2 | import config from "../config"; 3 | 4 | let f = async (ctx, next) => { 5 | try { 6 | //检查是否有密码 7 | if (checkToken(ctx)) { 8 | await next(); 9 | } else { 10 | ctx.status = 401; 11 | ctx.body = "密钥验证错误"; 12 | } 13 | } catch (error: any) { 14 | if (error.status != undefined) { 15 | ctx.status = error.status; 16 | } else { 17 | ctx.status = 500; 18 | } 19 | ctx.body = error.message; 20 | log.error(error); 21 | } 22 | } 23 | 24 | function checkToken(ctx) { 25 | if (!config.token) { 26 | return true; 27 | } 28 | let requestPath = ctx.method + ctx.path.replace(config.urlPrefix, ""); 29 | if (config.publicPath.has(requestPath)) { 30 | return true; 31 | } 32 | return config.token == ctx.headers.token; 33 | 34 | } 35 | 36 | export default f; -------------------------------------------------------------------------------- /openRenamerBackend/middleware/init.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import ObjectHelper from '../util/ObjectOperate'; 3 | 4 | let doSuccess = (ctx, body) => { 5 | switch (ctx.method) { 6 | case 'GET': 7 | ctx.status = body !== null ? 200 : 204; 8 | ctx.body = body; 9 | break; 10 | case 'POST': 11 | ctx.status = body !== null ? 201 : 204; 12 | ctx.body = body; 13 | break; 14 | case 'PUT': 15 | ctx.status = body !== null ? 200 : 204; 16 | ctx.body = body; 17 | break; 18 | case 'DELETE': 19 | ctx.status = body !== null ? 200 : 204; 20 | ctx.body = body; 21 | break; 22 | } 23 | Object.assign(ctx.allParams, ctx.params); 24 | } 25 | 26 | export default async (ctx, next) => { 27 | //跨域 28 | ctx.set("Access-Control-Allow-Origin", "*"); 29 | ctx.set("Access-Control-Allow-Headers", "X-Requested-With"); 30 | ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); 31 | ctx.set("X-Powered-By", ' 3.2.1'); 32 | ctx.set("Content-Type", "application/json;charset=utf-8"); 33 | //合并请求参数到allParams 34 | let objs = new Array(); 35 | if (ctx.method == "POST" || ctx.method == "PUT") { 36 | if (ctx.request.body) { 37 | if (ctx.request.body.fields != undefined && ctx.request.body.files != undefined) { 38 | objs.push(ctx.request.body.fields, ctx.request.body.files); 39 | } else { 40 | objs.push(ctx.request.body); 41 | } 42 | } 43 | } 44 | objs.push(ctx.query); 45 | ctx.allParams = ObjectHelper.combineObject(objs); 46 | 47 | ctx.onSuccess = function (body = null) { 48 | doSuccess(ctx, body); 49 | }; 50 | await next(); 51 | } -------------------------------------------------------------------------------- /openRenamerBackend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nas_backup", 3 | "version": "1.0.0", 4 | "description": "文件备份用", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "fxb", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@types/fs-extra": "5.0.4", 13 | "@types/koa": "2.13.12", 14 | "@types/node": "11.13.4", 15 | "axios": "0.21.4", 16 | "fs-extra": "7.0.0", 17 | "koa": "2.5.3", 18 | "koa-body": "4.0.4", 19 | "koa-router": "7.4.0", 20 | "koa-static": "5.0.0", 21 | "koa2-cors": "2.0.6", 22 | "log4js": "6.3.0", 23 | "moment": "2.22.2", 24 | "sqlite": "4.0.23", 25 | "sqlite3": "5.0.2", 26 | "uuid": "3.3.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /openRenamerBackend/service/ApplicationRuleService.ts: -------------------------------------------------------------------------------- 1 | import ApplicationRule from '../entity/po/ApplicationRule'; 2 | import ApplicationRuleDao from '../dao/ApplicationRuleDao'; 3 | import GlobalConfigDao from '../dao/GlobalConfigDao'; 4 | 5 | import { DEFAULT_TEMPLETE_ID } from '../entity/constants/GlobalConfigCodeConstant'; 6 | import GlobalConfig from '../entity/po/GlobalConfig'; 7 | import ErrorHelper from '../util/ErrorHelper'; 8 | 9 | 10 | class ApplicationRuleService { 11 | static async saveOrAdd(ruleObj: ApplicationRule): Promise { 12 | ruleObj.updatedDate = Date.now(); 13 | if (!ruleObj.id) { 14 | //说明是新增 15 | ruleObj.createdDate = Date.now(); 16 | ruleObj.id = await ApplicationRuleDao.addOne(ruleObj); 17 | } else { 18 | //说明是修改 19 | await ApplicationRuleDao.updateOne(ruleObj); 20 | } 21 | return ruleObj; 22 | } 23 | 24 | static async getAll(): Promise> { 25 | return await ApplicationRuleDao.getAll(); 26 | } 27 | 28 | static async deleteById(id: number): Promise { 29 | //禁止删除默认模板 30 | let idStr = await GlobalConfigDao.getByCode(DEFAULT_TEMPLETE_ID); 31 | if (id.toString() === idStr) { 32 | throw ErrorHelper.Error400("禁止删除默认模板"); 33 | } 34 | await ApplicationRuleDao.delete(id); 35 | } 36 | 37 | /** 38 | * 获取默认模板 39 | */ 40 | static async getDefault(): Promise { 41 | let res: ApplicationRule; 42 | let idStr = await GlobalConfigDao.getByCode(DEFAULT_TEMPLETE_ID); 43 | if (idStr == null) { 44 | let templteList = await ApplicationRuleDao.getAll(); 45 | if (templteList.length == 0) { 46 | res = new ApplicationRule("默认模板", "此模板为系统创建", "[]"); 47 | await ApplicationRuleService.saveOrAdd(res); 48 | } else { 49 | res = templteList[0]; 50 | } 51 | await GlobalConfigDao.addOne(new GlobalConfig(DEFAULT_TEMPLETE_ID, res.id.toString(), "默认模板id")); 52 | } else { 53 | let templteList = await ApplicationRuleDao.getAll(); 54 | if (templteList.length == 0) { 55 | res = new ApplicationRule("默认模板", "此模板为系统创建", "[]"); 56 | await ApplicationRuleService.saveOrAdd(res); 57 | await GlobalConfigDao.updateOne(DEFAULT_TEMPLETE_ID, res.id.toString()); 58 | } else { 59 | let temp = templteList.filter(item => item.id.toString() === idStr); 60 | if (temp.length > 0) { 61 | res = temp[0]; 62 | } else { 63 | res = templteList[0]; 64 | await GlobalConfigDao.updateOne(DEFAULT_TEMPLETE_ID, res.id.toString()); 65 | } 66 | } 67 | } 68 | return res; 69 | } 70 | } 71 | 72 | export default ApplicationRuleService; 73 | -------------------------------------------------------------------------------- /openRenamerBackend/service/AutoPlanService.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs-extra'; 4 | 5 | import AutoPlanConfigDto from '../entity/dto/AutoPlanConfigDto'; 6 | import GlobalConfig from 'entity/po/GlobalConfig'; 7 | import GlobalConfigService from './GlobalConfigService'; 8 | import ErrorHelper from '../util/ErrorHelper'; 9 | import TimeUtil from '../util/TimeUtil'; 10 | import { isSub, isVideo } from '../util/MediaUtil'; 11 | import log from '../util/LogUtil'; 12 | const autoConfigCode = "autoConfig"; 13 | let isReadDir = false; 14 | /** 15 | * 需要处理的文件 16 | */ 17 | let needDeal = []; 18 | /** 19 | * 文件夹变更记录。key:变更前的目录,value:变更后的目录.当needDeal为空时清理pathMap 20 | */ 21 | let pathMap = {}; 22 | /** 23 | * 自动化配置 24 | */ 25 | let autoConfig: AutoPlanConfigDto = null; 26 | 27 | 28 | class AutoPlanService { 29 | 30 | static async init() { 31 | let str = await GlobalConfigService.getVal(autoConfigCode); 32 | if (str != null) { 33 | } else { 34 | autoConfig = JSON.parse(str); 35 | } 36 | setTimeout(async () => { 37 | while (true) { 38 | try { 39 | await TimeUtil.sleep(1000); 40 | await work(); 41 | } catch (err) { 42 | console.log(err); 43 | } 44 | } 45 | }, 1000); 46 | } 47 | 48 | /** 49 | * 保存配置 50 | */ 51 | static async saveAutoConfig(body: AutoPlanConfigDto): Promise { 52 | if (isReadDir) { 53 | throw ErrorHelper.Error400("正在处理中,请稍后再试"); 54 | } 55 | if (body.start) { 56 | if (body.paths.length == 0) { 57 | throw ErrorHelper.Error400("视频路径为空"); 58 | } 59 | if (body.rules.length == 0) { 60 | throw ErrorHelper.Error400("规则为空"); 61 | } 62 | } 63 | let configBody: GlobalConfig = { 64 | code: autoConfigCode, 65 | val: JSON.stringify(body), 66 | description: "自动化计划配置" 67 | }; 68 | await GlobalConfigService.insertOrReplace(configBody); 69 | autoConfig = body; 70 | if (body.start && !body.ignoreExist) { 71 | setTimeout(async () => { 72 | isReadDir = true; 73 | try { 74 | await readDir(body.paths); 75 | } finally { 76 | isReadDir = false; 77 | } 78 | }, 1); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * 读取目录,获取文件列表 85 | * @param dirList 要读取的目录 86 | */ 87 | async function readDir(dirList: Array): Promise { 88 | if (!dirList) { 89 | return; 90 | } 91 | for (let i in dirList) { 92 | let pathStr = dirList[i]; 93 | if (checkIgnore(path.basename(pathStr))) { 94 | continue; 95 | } 96 | if (!(await fs.stat(pathStr)).isDirectory()) { 97 | let fileName = path.basename(pathStr); 98 | let strs = fileName.split('.').reverse(); 99 | if (strs.length > 0 && (isSub(strs[0]) || isVideo(strs[1]))) { 100 | needDeal.push(pathStr); 101 | } 102 | continue; 103 | } 104 | let childs = null; 105 | try { 106 | childs = await fs.readdir(pathStr); 107 | } catch (error) { 108 | console.warn("读取报错:{}", error); 109 | } 110 | if (childs != null) { 111 | await readDir(childs.map(item => path.join(pathStr, item))); 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * 检查文件名是否被忽略的 118 | */ 119 | function checkIgnore(str: string): boolean { 120 | for (let i in autoConfig.ignorePaths) { 121 | if (str.match(autoConfig.ignorePaths[i])) { 122 | return true; 123 | } 124 | } 125 | return false; 126 | } 127 | 128 | /** 129 | * 开始处理 130 | */ 131 | async function work() { 132 | if (autoConfig == null || !autoConfig.start) { 133 | return; 134 | } 135 | while (needDeal.length > 0) { 136 | let file = needDeal.pop(); 137 | try { 138 | await dealOnePath(file); 139 | } catch (error) { 140 | log.error("处理文件报错:{}", file); 141 | console.error(error); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * 处理一个文件路径 148 | * @param filePath 路径 149 | * @returns 150 | */ 151 | async function dealOnePath(filePath: string) { 152 | let exist = await fs.pathExists(filePath); 153 | if (!exist) { 154 | return; 155 | } 156 | let basePath = null; 157 | for (let i in autoConfig.paths) { 158 | if (filePath.startsWith(autoConfig.paths[i])) { 159 | basePath = autoConfig.paths[i]; 160 | break; 161 | } 162 | } 163 | if (basePath == null) { 164 | log.warn("无法识别的文件:{}", filePath); 165 | return; 166 | } 167 | let relativePath = filePath.replace(basePath, ""); 168 | let pathArrs = relativePath.split(path.sep).filter(item => item.length > 0); 169 | 170 | } 171 | 172 | export default AutoPlanService; 173 | -------------------------------------------------------------------------------- /openRenamerBackend/service/FileService.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs-extra'; 4 | 5 | import ProcessHelper from '../util/ProcesHelper'; 6 | import FileObj from '../entity/vo/FileObj'; 7 | import SavePathDao from '../dao/SavePathDao'; 8 | import SavePath from '../entity/po/SavePath'; 9 | import ErrorHelper from "../util/ErrorHelper"; 10 | 11 | let numberSet = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); 12 | 13 | class FileService { 14 | static async readPath(pathStr: string, showHidden: boolean): Promise> { 15 | pathStr = decodeURIComponent(pathStr); 16 | let fileList = []; 17 | if (pathStr.trim().length == 0) { 18 | //获取根目录路径 19 | if (config.isWindows) { 20 | //windows下 21 | let std: string = (await ProcessHelper.exec('wmic logicaldisk get caption')).replace('Caption', ''); 22 | fileList = std 23 | .split('\r\n') 24 | .filter((item) => item.trim().length > 0) 25 | .map((item) => item.trim()); 26 | } else { 27 | //linux下 28 | pathStr = '/'; 29 | fileList = await fs.readdir(pathStr); 30 | } 31 | } else { 32 | if (!(fs.pathExists(pathStr))) { 33 | throw new Error("路径不存在"); 34 | } 35 | fileList = await fs.readdir(pathStr); 36 | } 37 | let folderList: Array = []; 38 | let files: Array = []; 39 | for (let index in fileList) { 40 | try { 41 | let stat = await fs.stat(path.join(pathStr, fileList[index])); 42 | if (fileList[index].startsWith('.')) { 43 | if (showHidden) { 44 | (stat.isDirectory() ? folderList : files).push( 45 | new FileObj(fileList[index], pathStr, stat.isDirectory(), stat.size, stat.birthtime.getTime(), stat.mtime.getTime()), 46 | ); 47 | } 48 | } else { 49 | (stat.isDirectory() ? folderList : files).push( 50 | new FileObj(fileList[index], pathStr, stat.isDirectory(), stat.size, stat.birthtime.getTime(), stat.mtime.getTime()), 51 | ); 52 | } 53 | } catch (e) { 54 | console.error(e); 55 | } 56 | } 57 | folderList.sort((a, b) => FileService.compareStr(a.name, b.name)).push(...files.sort((a, b) => FileService.compareStr(a.name, b.name))); 58 | return folderList; 59 | } 60 | 61 | /** 62 | * 递归读取文件夹下所有的文件 63 | */ 64 | static async readRecursion(folders: Array): Promise> { 65 | let res = []; 66 | await this.readDirRecursion(res, folders, 1); 67 | return res; 68 | } 69 | 70 | private static async readDirRecursion(res: Array, folders: Array, depth: number): Promise { 71 | if (depth > 10) { 72 | throw ErrorHelper.Error400("递归读取超过10层,强制结束"); 73 | } 74 | if (folders == null || folders.length == 0) { 75 | return; 76 | } 77 | for (let i in folders) { 78 | let file = folders[i]; 79 | if (!file.isFolder) { 80 | res.push(file); 81 | } else { 82 | let filePath = path.join(file.path, file.name); 83 | let temp = (await fs.readdir(filePath)).map(item => { 84 | let stat = fs.statSync(path.join(filePath, item)); 85 | return new FileObj(item, filePath, stat.isDirectory(), stat.size, stat.birthtime.getTime(), stat.mtime.getTime()); 86 | }); 87 | await FileService.readDirRecursion(res, temp, depth + 1); 88 | } 89 | } 90 | } 91 | 92 | static async checkExist(pathStr: string) { 93 | return await fs.pathExists(pathStr); 94 | } 95 | 96 | /** 97 | * 收藏路径 98 | * @param saveObj 99 | * @returns 100 | */ 101 | static async savePath(saveObj: SavePath) { 102 | await SavePathDao.addOne(saveObj); 103 | return saveObj; 104 | } 105 | 106 | /** 107 | * 获取保存列表 108 | * @returns 109 | */ 110 | static async getSaveList() { 111 | return await SavePathDao.getAll(); 112 | } 113 | 114 | /** 115 | * 删除 116 | * @param id 117 | * @returns 118 | */ 119 | static async deleteOne(id) { 120 | return await SavePathDao.delete(id); 121 | } 122 | 123 | /** 124 | * 数字字母混合排序 125 | * @param a str 126 | * @param b str 127 | */ 128 | static compareStr(a: string, b: string) { 129 | let an = a.length; 130 | let bn = b.length; 131 | for (let i = 0; i < an;) { 132 | let charA = FileService.readChar(a, i, an); 133 | let charB = FileService.readChar(b, i, bn); 134 | if (charB.length == 0) { 135 | return 1; 136 | } 137 | if (charA !== charB) { 138 | //读取字符串不相等说明可以得到排序结果 139 | //如果都为数字,按照数字的比较方法,否则按照字符串比较 140 | return numberSet.has(charA.charAt(0)) && numberSet.has(charB.charAt(0)) ? Number(charA) - Number(charB) : charA.localeCompare(charB); 141 | } 142 | i += charA.length; 143 | } 144 | //排到最后都没分结果说明相等 145 | return 0; 146 | } 147 | 148 | /** 149 | * 读取字符,如果字符为数字就读取整个数字 150 | * @param a a 151 | * @param n 数字长度 152 | */ 153 | static readChar(a: string, i: number, n: number) { 154 | let res = ""; 155 | for (; i < n; i++) { 156 | let char = a.charAt(i); 157 | if (numberSet.has(char)) { 158 | //如果当前字符是数字,添加到结果中 159 | res += char; 160 | } else { 161 | //如果不为数字,但是为第一个字符,直接返回,否则返回res 162 | if (res.length == 0) { 163 | return char; 164 | } else { 165 | return res; 166 | } 167 | } 168 | } 169 | return res; 170 | } 171 | 172 | /** 173 | * delete batch 174 | * @param files files 175 | */ 176 | static async deleteBatch(files: Array): Promise { 177 | if (files == null || files.length == 0) { 178 | return; 179 | } 180 | for (let i in files) { 181 | await fs.remove(path.join(files[i].path, files[i].name)); 182 | } 183 | } 184 | 185 | /** 186 | * rename file from source to target 187 | * @param source sourceFile 188 | * @param target targetFile 189 | */ 190 | static async rename(source: FileObj, target: FileObj): Promise { 191 | await fs.rename(path.join(source.path, source.name), path.join(target.path, target.name)); 192 | } 193 | } 194 | 195 | export default FileService; 196 | -------------------------------------------------------------------------------- /openRenamerBackend/service/GlobalConfigService.ts: -------------------------------------------------------------------------------- 1 | import GlobalConfigDao from '../dao/GlobalConfigDao'; 2 | 3 | import { DEFAULT_TEMPLETE_ID } from '../entity/constants/GlobalConfigCodeConstant'; 4 | import GlobalConfig from '../entity/po/GlobalConfig'; 5 | 6 | 7 | class GlobalConfigService { 8 | 9 | static async getVal(code: string): Promise { 10 | return GlobalConfigDao.getByCode(code); 11 | } 12 | 13 | /** 14 | * 获取多个配置 15 | * @param codes codes 16 | * @returns 17 | */ 18 | static async getMultVal(codes: Array): Promise { 19 | let re = {}; 20 | (await GlobalConfigDao.getByMulCode(codes)).forEach(item => re[item.code] = item.val); 21 | return re; 22 | } 23 | 24 | static async updateVal(code: string, val: string): Promise { 25 | return GlobalConfigDao.updateOne(code, val); 26 | } 27 | 28 | static async insertOrReplace(body: GlobalConfig): Promise { 29 | return GlobalConfigDao.insertOrReplace(body); 30 | } 31 | } 32 | 33 | export default GlobalConfigService; 34 | -------------------------------------------------------------------------------- /openRenamerBackend/service/QbService.ts: -------------------------------------------------------------------------------- 1 | import QbConfigDto from "../entity/dto/QbConfigDto"; 2 | import {tryLogin, get, post, updateQbInfo, getQbInfo} from '../util/QbApiUtil'; 3 | import GlobalConfigService from "./GlobalConfigService"; 4 | import GlobalConfig from "../entity/po/GlobalConfig"; 5 | import BtListItemDto from "../entity/dto/BtListItemDto"; 6 | 7 | class QbService { 8 | 9 | /** 10 | * 保存地址 11 | * @param body 12 | */ 13 | static async saveAddress(body: QbConfigDto): Promise { 14 | if (body.address.endsWith("/")) { 15 | body.address = body.address.substring(0, body.address.length - 1); 16 | } 17 | await GlobalConfigService.insertOrReplace(new GlobalConfig("qbConfig", JSON.stringify(body), "qb config")); 18 | updateQbInfo(body); 19 | body.valid = await tryLogin(); 20 | body.version = body ? (await get("/app/version", null)) : null; 21 | return body; 22 | } 23 | a 24 | /** 25 | * 获取当前配置 26 | */ 27 | static async getConfig(): Promise { 28 | return getQbInfo(); 29 | } 30 | 31 | /** 32 | * get torrents list from qb 33 | */ 34 | static async getBtList(): Promise> { 35 | let res = await get("/api/v2/torrents/info?category=&sort=added_on", null); 36 | return res; 37 | } 38 | 39 | /** 40 | * 初始化 41 | */ 42 | static async init() { 43 | let config = await GlobalConfigService.getVal("qbConfig"); 44 | let qbInfo: QbConfigDto = config == null ? {} : JSON.parse(config); 45 | updateQbInfo(qbInfo); 46 | qbInfo.valid = await tryLogin(); 47 | qbInfo.version = qbInfo.valid ? (await get("/app/version", null)) : null; 48 | return qbInfo; 49 | } 50 | } 51 | 52 | export default QbService; 53 | -------------------------------------------------------------------------------- /openRenamerBackend/service/RenamerService.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs-extra'; 4 | 5 | import FileObj from '../entity/vo/FileObj'; 6 | import RuleObj from '../entity/vo/RuleObj'; 7 | import RuleInterface from '../entity/bo/rules/RuleInterface'; 8 | 9 | 10 | class RenamerService { 11 | static async preview(fileList: Array, ruleList: Array): Promise> { 12 | let ruleObjs = ruleList.map(item => new RuleObj(item)); 13 | let newNameSet: Set = new Set(); 14 | for (let i in fileList) { 15 | let obj = fileList[i]; 16 | ruleObjs.forEach(item => (item.data as RuleInterface).deal(obj)); 17 | if (newNameSet.has(obj.path + obj.name)) { 18 | obj.errorMessage = "重名"; 19 | } 20 | newNameSet.add(obj.path + obj.name); 21 | } 22 | return fileList; 23 | } 24 | 25 | static async rename(fileList: Array, changedFileList: Array) { 26 | for (let i in fileList) { 27 | let old = fileList[i]; 28 | let oldPath = path.join(fileList[i].path, fileList[i].name); 29 | let newPath = path.join(changedFileList[i].path, changedFileList[i].name); 30 | if (oldPath === newPath) { 31 | continue; 32 | } 33 | if ((await fs.pathExists(newPath))) { 34 | throw new Error("此路径已存在:" + newPath); 35 | } 36 | await fs.rename(oldPath, newPath); 37 | } 38 | } 39 | 40 | 41 | } 42 | 43 | export default RenamerService; 44 | -------------------------------------------------------------------------------- /openRenamerBackend/sqls/v001__init.sql: -------------------------------------------------------------------------------- 1 | -- 初始化建表 2 | -- 应用规则表 3 | CREATE TABLE application_rule ( 4 | -- 创建时间 5 | createdDate INTEGER NOT NULL, 6 | -- 更新时间 7 | updatedDate INTEGER NOT NULL, 8 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 9 | name TEXT NOT NULL, 10 | -- 注释 11 | comment TEXT NOT NULL DEFAULT '', 12 | -- 规则内容,json序列化后保存 13 | content TEXT NOT NULL DEFAULT '' 14 | ); -------------------------------------------------------------------------------- /openRenamerBackend/sqls/v002__init.sql: -------------------------------------------------------------------------------- 1 | -- 路径收藏表 2 | CREATE TABLE path_save ( 3 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | name TEXT NOT NULL, 5 | -- 路径内容 6 | content TEXT NOT NULL DEFAULT '' 7 | ); -------------------------------------------------------------------------------- /openRenamerBackend/sqls/v003_新增默认模板.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE global_config ( 2 | code TEXT(40), 3 | val TEXT(200), 4 | description TEXT(100) DEFAULT (''), 5 | CONSTRAINT global_config_PK PRIMARY KEY (code) 6 | ); -------------------------------------------------------------------------------- /openRenamerBackend/sqls/v004_新增记录表.sql: -------------------------------------------------------------------------------- 1 | -- 记录已处理过的路径 2 | CREATE TABLE dealed_file_path ( 3 | key_str TEXT(32) NOT NULL, 4 | "path" TEXT(200) NOT NULL, 5 | CONSTRAINT dealed_file_path_PK PRIMARY KEY (key_str) 6 | ); 7 | 8 | CREATE TABLE auto_deal_history ( 9 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 10 | createTime INTEGER NOT NULL, 11 | oldName TEXT(200) NOT NULL, 12 | newName TEXT(200) NOT NULL, 13 | -- 说明 14 | comment TEXT(200) NOT NULL, 15 | -- 1:文件重命名,2:剧集下无季文件夹,自动创建;3:操作失败 16 | "type" INTEGER NOT NULL 17 | ); 18 | 19 | CREATE INDEX auto_deal_history_createTime_IDX ON auto_deal_history (createTime); -------------------------------------------------------------------------------- /openRenamerBackend/sqls/v005_新增推荐规则.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO application_rule (createdDate, updatedDate, name, comment, content ) VALUES (1669648328180, 1678279879110, '推荐剧集模板', '此模板为系统创建12121212', '[{"type":"delete","message":"删除:全部删除","data":{"type":"deleteAll","start":{"type":"location","value":"1"},"end":{"type":"location","value":"1"},"ignorePostfix":true},"checked":false},{"type":"auto","message":"自动识别:\"剧名/电影名识别\";","data":{"type":"name","frontAdd":"","endAdd":"","eNumWidth":2},"checked":false},{"type":"auto","message":"自动识别:\"季号识别\";前缀添加:.s","data":{"type":"season","frontAdd":".s","endAdd":"","eNumWidth":2},"checked":false},{"type":"auto","message":"自动识别:\"集数识别\";集数宽度:3;前缀添加:e","data":{"type":"eNum","frontAdd":"e","endAdd":"","eNumWidth":3},"checked":false},{"type":"auto","message":"自动识别:\"分辨率识别\";前缀添加:.","data":{"type":"resolution","frontAdd":".","endAdd":"","eNumWidth":2},"checked":false}]'); 2 | -------------------------------------------------------------------------------- /openRenamerBackend/start.sh: -------------------------------------------------------------------------------- 1 | pnpm install --registry https://registry.npmmirror.com && tsc && node dist/index.js 2 | -------------------------------------------------------------------------------- /openRenamerBackend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/51c6ec998677e02fe4973d7246d4cacc9d64f53a/openRenamerBackend/static/.gitkeep -------------------------------------------------------------------------------- /openRenamerBackend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "noImplicitAny": false, 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "baseUrl": ".", 9 | "rootDir": "./", 10 | "strict": true, 11 | "strictNullChecks": false, 12 | "esModuleInterop": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /openRenamerBackend/util/ErrorHelper.ts: -------------------------------------------------------------------------------- 1 | class ErrorHelper { 2 | /** 3 | * 返回一个自定义错误 4 | * @param {String} message 5 | * @param {Number} status 6 | */ 7 | static newError(message, status) { 8 | return getError(message, status); 9 | } 10 | 11 | static Error403(message){ 12 | return getError(message,403); 13 | } 14 | static Error404(message){ 15 | return getError(message,404); 16 | } 17 | static Error406(message){ 18 | return getError(message,406); 19 | } 20 | static Error400(message){ 21 | return getError(message,400); 22 | } 23 | 24 | } 25 | 26 | let getError = (message, status) => { 27 | let error = new Error(message); 28 | error['status'] = status; 29 | return error; 30 | } 31 | 32 | export default ErrorHelper; -------------------------------------------------------------------------------- /openRenamerBackend/util/LogUtil.ts: -------------------------------------------------------------------------------- 1 | import { getLogger, configure } from "log4js"; 2 | configure({ 3 | appenders: { cheese: { type: "console" } }, 4 | categories: { default: { appenders: ["cheese"], level: "info" } } 5 | }); 6 | const logger = getLogger(); 7 | logger.level = "debug"; 8 | 9 | export default logger; -------------------------------------------------------------------------------- /openRenamerBackend/util/MediaUtil.ts: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const videoSet = new Set(["flv", 'avi', 'wmv', 'dat', 'vob', 'mpg', 'mpeg', 'mp4', '3gp', '3g2', 'mkv', 'rm', 'rmvb', 'mov', 'qt', 'ogg', 'ogv', 'oga', 'mod']); 3 | 4 | /** 5 | * 判断文件后缀是否为视频类型 6 | * @param str 文件后缀 7 | */ 8 | export function isVideo(str: string) { 9 | if (!str) { 10 | return false; 11 | } 12 | return videoSet.has(str.toLowerCase()); 13 | } 14 | 15 | const subSet = new Set(['sub', 'sst', 'son', 'srt', 'ssa', 'ass', 'smi', 'psb', 'pjs', 'stl', 'tts', 'vsf', 'zeg']); 16 | 17 | /** 18 | * 判断文件是否为字幕文件 19 | * @param str 文件后缀 20 | */ 21 | export function isSub(str: string) { 22 | if (!str) { 23 | return false; 24 | } 25 | return subSet.has(str.toLowerCase()); 26 | } 27 | 28 | /** 29 | * 判断文件是否为字幕文件 30 | * @param str 文件后缀 31 | */ 32 | export function isNfo(str: string) { 33 | if (!str) { 34 | return false; 35 | } 36 | return "nfo" == str; 37 | } 38 | 39 | let pattern1 = new RegExp(/s(eason)?\.?(\d+)/); 40 | let pattern2 = new RegExp(/(\d+)/); 41 | let pattern3 = new RegExp(/([一二三四五六七八九十]+)/); 42 | let chineseNumMap = { 43 | "一": "1", 44 | "二": "2", 45 | "三": "3", 46 | "四": "4", 47 | "五": "5", 48 | "六": "6", 49 | "七": "7", 50 | "八": "8", 51 | "九": "9", 52 | "十": "1" 53 | 54 | } 55 | 56 | /** 57 | * 识别季号 58 | * @param name 59 | */ 60 | export function getSeason(name: string): string { 61 | name = name.replace(/[ ]+/, "").toLocaleLowerCase(); 62 | let patternRes = name.match(pattern1); 63 | if (patternRes && patternRes[2]) { 64 | return patternRes[2]; 65 | } 66 | patternRes = name.match(pattern2); 67 | if (patternRes && patternRes[1]) { 68 | return patternRes[1]; 69 | } 70 | //中文支持 71 | patternRes = name.match(pattern3); 72 | if (patternRes && patternRes[1]) { 73 | let str = patternRes[1]; 74 | let strs = str.split(""); 75 | if (strs.length == 1) { 76 | return str == '十' ? "10" : chineseNumMap[str]; 77 | } else if (strs.length == 2) { 78 | return strs[0] == '十' ? ("1" + chineseNumMap[strs[1]]) : chineseNumMap[strs[0]] + "0"; 79 | } else if (strs.length == 3) { 80 | return chineseNumMap[strs[0]] + chineseNumMap[strs[2]]; 81 | } 82 | } 83 | return ""; 84 | } -------------------------------------------------------------------------------- /openRenamerBackend/util/NumberUtil.ts: -------------------------------------------------------------------------------- 1 | 2 | class NumberUtil { 3 | static getRandom(min: number, max: number): number { 4 | return Math.floor((Math.random() * (max - min + 1) + min)); 5 | } 6 | } 7 | 8 | export default NumberUtil; -------------------------------------------------------------------------------- /openRenamerBackend/util/ObjectOperate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 合并node对象,对于相同的属性后面覆盖前面 3 | */ 4 | class ObjectOperation { 5 | static combineObject(...objs) { 6 | if (objs.length == 1 && objs[0] instanceof Array) { 7 | objs = objs[0]; 8 | } 9 | let sum = {}; 10 | let length = objs.length; 11 | for (let i = 0; i < length; i++) { 12 | sum = Object.assign(sum,objs[i]); 13 | } 14 | return sum; 15 | } 16 | } 17 | 18 | export default ObjectOperation -------------------------------------------------------------------------------- /openRenamerBackend/util/ProcesHelper.ts: -------------------------------------------------------------------------------- 1 | import * as childPrecess from 'child_process'; 2 | 3 | class ProcessHelper { 4 | static exec(cmd): Promise { 5 | return new Promise((resolve, reject) => { 6 | childPrecess.exec(cmd, (error, stdout, stderr) => { 7 | if (error) { 8 | reject(error); 9 | } if (stderr) { 10 | reject(stderr); 11 | } else { 12 | resolve(stdout) 13 | } 14 | }) 15 | }) 16 | } 17 | } 18 | 19 | // (async()=>{ 20 | // let res= await ProcessHelper.exec('cd /d e://workspace&&dir'); 21 | // console.log(res); 22 | // })() 23 | 24 | export default ProcessHelper -------------------------------------------------------------------------------- /openRenamerBackend/util/QbApiUtil.ts: -------------------------------------------------------------------------------- 1 | import {Method} from "axios"; 2 | import axios from "axios"; 3 | import querystring from "querystring"; 4 | import QbConfigDto from "../entity/dto/QbConfigDto"; 5 | import GlobalService from '../service/GlobalConfigService'; 6 | 7 | let qbInfo: QbConfigDto = null; 8 | let cookie: any = null; 9 | 10 | export function updateQbInfo(info: QbConfigDto) { 11 | qbInfo = info; 12 | } 13 | 14 | export function getQbInfo() { 15 | return qbInfo; 16 | } 17 | 18 | export async function get(url: string, data: object) { 19 | return await request("get", url, data, null, false); 20 | } 21 | 22 | export async function post(url: string, data: object, isForm = false) { 23 | return await request("post", url, null, data, isForm); 24 | } 25 | 26 | async function request(method: Method, url: string, query: any, body: any, isForm = false) { 27 | if (!qbInfo.valid) { 28 | throw new Error("qbittorrent无法连接,请检查配置"); 29 | } 30 | let isTryLogin = false; 31 | while (true) { 32 | let headers = {"Cookie": cookie}; 33 | if (isForm) { 34 | headers['content-type'] = "multipart/form-data"; 35 | } else if (method == "post") { 36 | headers['content-type'] = "application/json"; 37 | } 38 | let res = await axios.request({ 39 | baseURL: qbInfo.address, 40 | url: "/api/v2" + url, 41 | method, 42 | params: query, 43 | data: body, 44 | headers, 45 | }); 46 | if (res.status == 200) { 47 | return res.data; 48 | } 49 | if (res.status == 403) { 50 | if (isTryLogin) { 51 | throw new Error("qb用户名密码设置有误"); 52 | } else { 53 | await tryLogin(); 54 | isTryLogin = true; 55 | } 56 | } else { 57 | throw new Error("请求报错:" + res.data); 58 | } 59 | } 60 | 61 | } 62 | 63 | export async function tryLogin(): Promise { 64 | if (qbInfo == null || qbInfo.address == null || qbInfo.address == "") { 65 | return false; 66 | } 67 | let body = {username: qbInfo.username, password: qbInfo.password}; 68 | try { 69 | let res = await axios.post(qbInfo.address + `/api/v2/auth/login`, querystring.stringify(body), { 70 | headers: {"Content-Type": "application/x-www-form-urlencoded"} 71 | }); 72 | let success = res.data.toLocaleLowerCase().indexOf('ok') > -1; 73 | if (success) { 74 | cookie = res.headers['set-cookie']; 75 | } 76 | qbInfo.valid = success; 77 | return success; 78 | } catch (error) { 79 | console.error("登录报错:", error); 80 | return false; 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /openRenamerBackend/util/SqliteHelper.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import {open, Database} from 'sqlite'; 3 | import config from '../config'; 4 | import * as fs from "fs-extra"; 5 | import * as path from 'path'; 6 | import log from './LogUtil'; 7 | 8 | const HISTORY_NAME = "history.json"; 9 | 10 | 11 | class SqliteHelper { 12 | public static pool: Database = null; 13 | 14 | static async createPool() { 15 | let dataFolder = config.dataPath; 16 | if (!fs.existsSync(dataFolder)) { 17 | await fs.mkdir(dataFolder); 18 | } 19 | SqliteHelper.pool = await open({ 20 | filename: path.join(dataFolder, "database.db"), 21 | driver: sqlite3.Database 22 | }); 23 | let basePath = path.join(config.rootPath, "sqls"); 24 | let hisPath = path.join(dataFolder, HISTORY_NAME); 25 | let history: Array; 26 | if (fs.existsSync(hisPath)) { 27 | history = JSON.parse(await fs.readFile(hisPath, "utf-8")); 28 | } else { 29 | history = []; 30 | } 31 | //执行数据库 32 | let files = (await fs.readdir(basePath)).sort((a, b) => a.localeCompare(b)).filter(item => !(item === HISTORY_NAME)); 33 | let error = null; 34 | for (let i = 0; i < files.length; i++) { 35 | if (history.indexOf(files[i]) > -1) { 36 | log.info("sql无需重复执行:", files[i]); 37 | continue; 38 | } 39 | let sqlLines = (await fs.readFile(path.join(basePath, files[i]), 'utf-8')).split(/[\r\n]/g).map(item => item.trim()).filter(item => !item.startsWith("--")); 40 | try { 41 | let sql = ""; 42 | for (let j = 0; j < sqlLines.length; j++) { 43 | sql = sql + " " + sqlLines[j]; 44 | if (sqlLines[j].endsWith(";")) { 45 | await SqliteHelper.pool.run(sql); 46 | sql = ""; 47 | } 48 | } 49 | log.info("sql执行成功:", files[i]); 50 | history.push(files[i]); 51 | } catch (err) { 52 | error = err; 53 | break; 54 | } 55 | } 56 | await fs.writeFile(hisPath, JSON.stringify(history)); 57 | if (error != null) { 58 | throw error; 59 | } 60 | } 61 | } 62 | 63 | export default SqliteHelper; 64 | -------------------------------------------------------------------------------- /openRenamerBackend/util/TimeUtil.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | class TimeUtil { 3 | /** 4 | * 获取今天的零点 5 | */ 6 | static getZeroTime(): Date { 7 | return moment() 8 | .millisecond(0) 9 | .second(0) 10 | .minute(0) 11 | .hour(0) 12 | .toDate(); 13 | } 14 | 15 | static async sleep(duration: number): Promise { 16 | return new Promise((resolve, reject) => { 17 | setTimeout(() => resolve(), duration); 18 | }); 19 | } 20 | } 21 | 22 | export default TimeUtil; 23 | -------------------------------------------------------------------------------- /openRenamerBackend/util/TranslateUtil.ts: -------------------------------------------------------------------------------- 1 | // ToolGood.Words.Translate.js 2 | // 2020, Lin Zhijun, https://github.com/toolgood/ToolGood.Words 3 | // Licensed under the Apache License 2.0 4 | import {_s2t_s, _s2t_t, _t2hk_t, _t2hk_hk, _t2s_s, _t2tw_t, _t2s_t, _t2tw_tw} from './TranslateWord' 5 | 6 | class TrieNode { 7 | Index = 0; 8 | Layer = 0; 9 | End = false; 10 | Char = ''; 11 | Results = []; 12 | m_values = {}; 13 | Failure = null; 14 | Parent = null; 15 | 16 | public Add(c) { 17 | if (this.m_values[c] != null) { 18 | return this.m_values[c]; 19 | } 20 | var node = new TrieNode(); 21 | node.Parent = this; 22 | node.Char = c; 23 | this.m_values[c] = node; 24 | return node; 25 | } 26 | 27 | 28 | public SetResults(index) { 29 | if (this.End == false) { 30 | this.End = true; 31 | } 32 | this.Results.push(index) 33 | } 34 | } 35 | 36 | 37 | class TrieNode2 { 38 | End = false; 39 | Results = []; 40 | m_values = {}; 41 | minflag = 1; 42 | maxflag = 0; 43 | 44 | public Add(c, node3) { 45 | if (typeof c !== 'number') { 46 | c = parseInt(c); 47 | } 48 | if (this.minflag > c) { 49 | this.minflag = c; 50 | } 51 | if (this.maxflag < c) { 52 | this.maxflag = c; 53 | } 54 | this.m_values[c] = node3; 55 | } 56 | 57 | public SetResults(index) { 58 | if (this.End == false) { 59 | this.End = true; 60 | } 61 | if (this.Results.indexOf(index) == -1) { 62 | this.Results.push(index); 63 | } 64 | } 65 | 66 | public HasKey(c) { 67 | return this.m_values[c] != undefined; 68 | } 69 | 70 | public TryGetValue(c) { 71 | if (this.minflag <= c && this.maxflag >= c) { 72 | return this.m_values[c]; 73 | } 74 | return null; 75 | } 76 | } 77 | 78 | 79 | class WordsSearch { 80 | 81 | 82 | _first: TrieNode2 = null; 83 | _keywords = []; 84 | _others = []; 85 | 86 | public SetKeywords(keywords) { 87 | this._keywords = keywords; 88 | let root = new TrieNode(); 89 | 90 | let allNodeLayer = {}; 91 | for (let i = 0; i < this._keywords.length; i++) { 92 | let p = this._keywords[i]; 93 | let nd = root; 94 | for (let j = 0; j < p.length; j++) { 95 | nd = nd.Add(p.charCodeAt(j)); 96 | if (nd.Layer == 0) { 97 | nd.Layer = j + 1; 98 | if (allNodeLayer[nd.Layer]) { 99 | allNodeLayer[nd.Layer].push(nd) 100 | } else { 101 | allNodeLayer[nd.Layer] = []; 102 | allNodeLayer[nd.Layer].push(nd) 103 | } 104 | } 105 | } 106 | nd.SetResults(i); 107 | } 108 | 109 | let allNode: TrieNode[] = []; 110 | allNode.push(root); 111 | for (let key in allNodeLayer) { 112 | let nds = allNodeLayer[key]; 113 | for (let i = 0; i < nds.length; i++) { 114 | allNode.push(nds[i]); 115 | } 116 | } 117 | allNodeLayer = null; 118 | 119 | for (let i = 1; i < allNode.length; i++) { 120 | let nd: TrieNode = allNode[i]; 121 | nd.Index = i; 122 | let r = nd.Parent.Failure; 123 | let c = nd.Char; 124 | while (r != null && !r.m_values[c]) 125 | r = r.Failure; 126 | if (r == null) 127 | nd.Failure = root; 128 | else { 129 | nd.Failure = r.m_values[c]; 130 | for (let key2 in nd.Failure.Results) { 131 | if (nd.Failure.Results.hasOwnProperty(key2) == false) { 132 | continue; 133 | } 134 | let result = nd.Failure.Results[key2]; 135 | nd.SetResults(result); 136 | } 137 | } 138 | } 139 | root.Failure = root; 140 | 141 | let allNode2 = []; 142 | for (let i = 0; i < allNode.length; i++) { 143 | allNode2.push(new TrieNode2()); 144 | } 145 | for (let i = 0; i < allNode2.length; i++) { 146 | let oldNode = allNode[i]; 147 | let newNode = allNode2[i]; 148 | 149 | for (let key in oldNode.m_values) { 150 | if (oldNode.m_values.hasOwnProperty(key) == false) { 151 | continue; 152 | } 153 | let index = oldNode.m_values[key].Index; 154 | newNode.Add(key, allNode2[index]); 155 | } 156 | for (let index = 0; index < oldNode.Results.length; index++) { 157 | let item = oldNode.Results[index]; 158 | newNode.SetResults(item); 159 | } 160 | 161 | oldNode = oldNode.Failure; 162 | while (oldNode != root) { 163 | for (let key in oldNode.m_values) { 164 | if (oldNode.m_values.hasOwnProperty(key) == false) { 165 | continue; 166 | } 167 | if (newNode.HasKey(key) == false) { 168 | let index = oldNode.m_values[key].Index; 169 | newNode.Add(key, allNode2[index]); 170 | } 171 | } 172 | for (let index = 0; index < oldNode.Results.length; index++) { 173 | let item = oldNode.Results[index]; 174 | newNode.SetResults(item); 175 | } 176 | oldNode = oldNode.Failure; 177 | } 178 | } 179 | allNode = null; 180 | root = null; 181 | this._first = allNode2[0]; 182 | } 183 | 184 | public FindAll(text) { 185 | var ptr = null; 186 | var list = []; 187 | 188 | for (let i = 0; i < text.length; i++) { 189 | var t = text.charCodeAt(i); 190 | var tn = null; 191 | if (ptr == null) { 192 | tn = this._first.TryGetValue(t); 193 | } else { 194 | tn = ptr.TryGetValue(t); 195 | if (!tn) { 196 | tn = this._first.TryGetValue(t); 197 | } 198 | } 199 | if (tn != null) { 200 | if (tn.End) { 201 | for (let j = 0; j < tn.Results.length; j++) { 202 | var item = tn.Results[j]; 203 | var keyword = this._keywords[item]; 204 | list.push({ 205 | Keyword: keyword, 206 | Success: true, 207 | End: i, 208 | Start: i + 1 - this._keywords[item].length, 209 | Index: item, 210 | }); 211 | } 212 | } 213 | } 214 | ptr = tn; 215 | } 216 | return list; 217 | } 218 | } 219 | 220 | //--------------------------- 221 | 222 | 223 | //----------------------- 224 | var s2tSearch = null; // WordsSearch 225 | var t2sSearch = null;// WordsSearch 226 | var t2twSearch = null;// WordsSearch 227 | var tw2tSearch = null;// WordsSearch 228 | var t2hkSearch = null;// WordsSearch 229 | var hk2tSearch = null;// WordsSearch 230 | 231 | /** 232 | * 转繁体中文 233 | * @param {any} text 原文本 234 | * @param {any} type 0、繁体中文,1、港澳繁体,2、台湾正体 235 | */ 236 | export function toTraditionalChinese(text: any, type: any) { 237 | if (type == undefined) { 238 | type = 0; 239 | } 240 | if (type > 2 || type < 0) { 241 | throw "type 不支持该类型"; 242 | } 243 | 244 | var s2t = GetWordsSearch(true, 0); 245 | text = TransformationReplace(text, s2t); 246 | if (type > 0) { 247 | var t2 = GetWordsSearch(true, type); 248 | text = TransformationReplace(text, t2); 249 | } 250 | return text; 251 | } 252 | 253 | /** 254 | * 转简体中文 255 | * @param {any} text 原文本 256 | * @param {any} srcType 0、繁体中文,1、港澳繁体,2、台湾正体 257 | */ 258 | export function toSimplifiedChinese(text: string, srcType: any) { 259 | if (srcType == undefined) { 260 | srcType = 0; 261 | } 262 | if (srcType > 2 || srcType < 0) { 263 | throw "srcType 不支持该类型"; 264 | } 265 | if (srcType > 0) { 266 | var t2 = GetWordsSearch(false, srcType); 267 | text = TransformationReplace(text, t2); 268 | } 269 | var s2t = GetWordsSearch(false, 0); 270 | text = TransformationReplace(text, s2t); 271 | return text; 272 | } 273 | 274 | function TransformationReplace(text: any, wordsSearch: any) { 275 | var ts = wordsSearch.FindAll(text); 276 | 277 | var sb = ""; 278 | var index = 0; 279 | while (index < text.length) { 280 | var t = null; 281 | var max = -1; 282 | for (var i = 0; i < ts.length; i++) { 283 | var f = ts[i]; 284 | if (f.Start == index && f.End > max) { 285 | max = f.End; 286 | t = f; 287 | } 288 | } 289 | 290 | if (t == null) { 291 | sb += text[index]; 292 | index++; 293 | } else { 294 | sb += wordsSearch._others[t.Index] 295 | index = t.End + 1; 296 | } 297 | } 298 | return sb; 299 | } 300 | 301 | function GetWordsSearch(s2t: boolean, srcType: number) { 302 | if (s2t) { 303 | if (srcType === 0) { 304 | if (s2tSearch == null) { 305 | s2tSearch = BuildWordsSearch(_s2t_s, _s2t_t); 306 | } 307 | return s2tSearch; 308 | } else if (srcType === 1) { 309 | if (t2hkSearch == null) { 310 | t2hkSearch = BuildWordsSearch(_t2hk_t, _t2hk_hk); 311 | } 312 | return t2hkSearch; 313 | } else if (srcType == 2) { 314 | if (t2twSearch == null) { 315 | t2twSearch = BuildWordsSearch(_t2tw_t, _t2tw_tw); 316 | } 317 | return t2twSearch; 318 | } 319 | } 320 | if (srcType == 0) { 321 | if (t2sSearch == null) { 322 | t2sSearch = BuildWordsSearch(_t2s_t, _t2s_s); 323 | } 324 | return t2sSearch; 325 | } else if (srcType == 1) { 326 | if (hk2tSearch == null) { 327 | hk2tSearch = BuildWordsSearch(_t2hk_hk, _t2hk_t); 328 | } 329 | return hk2tSearch; 330 | } else if (srcType == 2) { 331 | if (tw2tSearch == null) { 332 | tw2tSearch = BuildWordsSearch(_t2tw_tw, _t2tw_t); 333 | } 334 | return tw2tSearch; 335 | } 336 | return null; 337 | } 338 | 339 | function BuildWordsSearch(keywords: string[], toWords: any[]) { 340 | var wordsSearch = new WordsSearch(); 341 | wordsSearch.SetKeywords(keywords); 342 | wordsSearch._others = toWords; 343 | return wordsSearch; 344 | } -------------------------------------------------------------------------------- /openRenamerBackend/util/ValUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * null to default 3 | * @param value 4 | * @param defaultVal 5 | */ 6 | export function nullToDefault(value: any, defaultVal: any): any { 7 | return value === undefined || value == null ? defaultVal : value; 8 | } -------------------------------------------------------------------------------- /openRenamerBackend/util/pathUtil.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path' 2 | 3 | class pathUtil { 4 | static getPath(pathStr) { 5 | return path.resolve(pathUtil.getRootPath(), pathStr); 6 | } 7 | 8 | static getRootPath() { 9 | return path.resolve(__dirname, '..'); 10 | } 11 | } 12 | 13 | 14 | export default pathUtil -------------------------------------------------------------------------------- /openRenamerFront/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /openRenamerFront/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | package-lock.json 5 | yarn.lock 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .pnpm-store -------------------------------------------------------------------------------- /openRenamerFront/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 2 4 | } -------------------------------------------------------------------------------- /openRenamerFront/README.md: -------------------------------------------------------------------------------- 1 | # front 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /openRenamerFront/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /openRenamerFront/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "ES6", 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /openRenamerFront/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-renamer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "2.0.10", 12 | "axios": "1.6.0", 13 | "core-js": "3.35.0", 14 | "dayjs": "1.10.7", 15 | "element-plus": "2.2.25", 16 | "vue": "3.2.45", 17 | "vue-router": "4.2.5" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "5.0.8", 21 | "@vue/cli-plugin-router": "5.0.8", 22 | "@vue/cli-service": "5.0.8", 23 | "@vue/compiler-sfc": "3.0.0", 24 | "less": "3.0.4", 25 | "less-loader": "5.0.0", 26 | "prettier": "2.2.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /openRenamerFront/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/51c6ec998677e02fe4973d7246d4cacc9d64f53a/openRenamerFront/public/favicon.ico -------------------------------------------------------------------------------- /openRenamerFront/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | renamer 9 | 10 | 11 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /openRenamerFront/src/App.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 84 | 85 | 137 | -------------------------------------------------------------------------------- /openRenamerFront/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/51c6ec998677e02fe4973d7246d4cacc9d64f53a/openRenamerFront/src/assets/logo.png -------------------------------------------------------------------------------- /openRenamerFront/src/components/FileChose.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 178 | 179 | 199 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/Rule.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 62 | 63 | 75 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/Tips.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/ApplicationRuleList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 118 | 119 | 122 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/AutoRule.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 97 | 98 | 116 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/DeleteRule.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 151 | 152 | 176 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/InsertRule.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 98 | 99 | 117 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/ReplaceRule.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 102 | 103 | 121 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/RuleBlock.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 189 | 190 | 215 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/SerializationRule.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 87 | 88 | 106 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/TranslateRule.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 59 | 60 | 78 | -------------------------------------------------------------------------------- /openRenamerFront/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import ElementPlus, { ElMessage } from "element-plus"; 5 | import "element-plus/dist/index.css"; 6 | 7 | const vueInstance = createApp(App); 8 | vueInstance.use(router).use(ElementPlus).mount("#app"); 9 | vueInstance.config.globalProperties.$message = ElMessage; 10 | window.vueInstance = vueInstance; 11 | -------------------------------------------------------------------------------- /openRenamerFront/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import Home from "../views/home/Home.vue"; 3 | import Login from "../views/public/login"; 4 | 5 | const routes = [ 6 | { 7 | path: "/", 8 | name: "Home", 9 | component: Home, 10 | }, { 11 | path: "/auto", 12 | name: "Auto", 13 | component: () => import("@/views/auto/index"), 14 | }, { 15 | path: "/download/config", 16 | name: "downloadConfig", 17 | component: () => import("@/views/download/config/index"), 18 | }, { 19 | path: "/public/login", 20 | name: "login", 21 | component: Login, 22 | }, 23 | ]; 24 | 25 | const router = createRouter({ 26 | history: createWebHistory(process.env.BASE_URL), 27 | routes, 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /openRenamerFront/src/utils/Bus.js: -------------------------------------------------------------------------------- 1 | class Bus { 2 | 3 | constructor() { 4 | 5 | this.list = { 6 | }; // 收集订阅 7 | } 8 | // 订阅 9 | $on (name, fn) { 10 | 11 | this.list[name] = this.list[name] || []; 12 | this.list[name].push(fn); 13 | } 14 | // 发布 15 | $emit (name, data) { 16 | 17 | if (this.list[name]) { 18 | 19 | this.list[name].forEach((fn) => { 20 | fn(data); 21 | }); 22 | } 23 | } 24 | // 取消订阅 25 | $off (name) { 26 | 27 | if (this.list[name]) { 28 | 29 | delete this.list[name]; 30 | } 31 | } 32 | } 33 | export default new Bus; -------------------------------------------------------------------------------- /openRenamerFront/src/utils/HttpUtil.js: -------------------------------------------------------------------------------- 1 | import * as http from 'axios'; 2 | import router from '../router/index'; 3 | 4 | /** 5 | * 请求 6 | * @param {*} url url 7 | * @param {*} method 方法 8 | * @param {*} params url参数 9 | * @param {*} body 请求体 10 | * @param {*} isForm 是否form 11 | * @param {*} redirect 接口返回未认证是否跳转到登陆 12 | * @returns 数据 13 | */ 14 | async function request(url, method, params, body, isForm) { 15 | let options = { 16 | url, 17 | baseURL: '/openRenamer/api', 18 | method, 19 | params, 20 | headers: {token: window.token} 21 | }; 22 | if (isForm) { 23 | options.headers['Content-Type'] = 'multipart/form-data'; 24 | } 25 | if (body) { 26 | options.data = body; 27 | } 28 | let res; 29 | try { 30 | res = await http.default.request(options); 31 | } catch (err) { 32 | console.log(Object.keys(err)); 33 | console.log(err.response); 34 | if (err.response.status === 401) { 35 | window.vueInstance.config.globalProperties.$message.error('密钥验证错误'); 36 | router.push("/public/login"); 37 | } else if (err.response.status === 400) { 38 | window.vueInstance.config.globalProperties.$message.error(err.response.data); 39 | } else { 40 | window.vueInstance.config.globalProperties.$message.error('发生了某些异常问题'); 41 | } 42 | throw err; 43 | } 44 | return res.data; 45 | } 46 | 47 | /** 48 | * get方法 49 | * @param {*} url url 50 | * @param {*} params url参数 51 | * @param {*} redirect 未登陆是否跳转到登陆页 52 | */ 53 | async function get(url, params = null) { 54 | return request(url, 'get', params, null, false); 55 | } 56 | 57 | /** 58 | * post方法 59 | * @param {*} url url 60 | * @param {*} params url参数 61 | * @param {*} body body参数 62 | * @param {*} isForm 是否表单数据 63 | * @param {*} redirect 是否重定向 64 | */ 65 | async function post(url, params, body, isForm = false) { 66 | return request(url, 'post', params, body, isForm); 67 | } 68 | 69 | /** 70 | * put方法 71 | * @param {*} url url 72 | * @param {*} params url参数 73 | * @param {*} body body参数 74 | * @param {*} isForm 是否表单数据 75 | * @param {*} redirect 是否重定向 76 | */ 77 | async function put(url, params, body, isForm = false) { 78 | return request(url, 'put', params, body, isForm); 79 | } 80 | 81 | /** 82 | * delete方法 83 | * @param {*} url url 84 | * @param {*} params url参数 85 | * @param {*} redirect 是否重定向 86 | */ 87 | async function deletes(url, params = null) { 88 | return request(url, 'delete', params, null); 89 | } 90 | 91 | export default { 92 | get, 93 | post, 94 | put, 95 | delete: deletes, 96 | }; 97 | -------------------------------------------------------------------------------- /openRenamerFront/src/utils/ValUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 空转default 3 | * @param val 4 | * @param defaultVal 5 | * @returns {*} 6 | */ 7 | export function nullToDefault(val, defaultVal) { 8 | return val === undefined || val == null ? defaultVal : val; 9 | } -------------------------------------------------------------------------------- /openRenamerFront/src/views/auto/components/editForm.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 148 | 149 | 165 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/auto/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | 56 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/download/config/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 47 | 48 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/home/Home.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 363 | 364 | 409 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/public/login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 41 | -------------------------------------------------------------------------------- /openRenamerFront/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: "http://localhost:8089", 4 | }, 5 | publicPath: "./" 6 | }; 7 | --------------------------------------------------------------------------------