├── config.json ├── openRenamerBackend ├── static │ └── .gitkeep ├── pnpm-workspace.yaml ├── start.sh ├── entity │ ├── constants │ │ └── GlobalConfigCodeConstant.ts │ ├── po │ │ ├── SavePath.ts │ │ ├── GlobalConfig.ts │ │ └── ApplicationRule.ts │ ├── dto │ │ ├── QbCommonDto.ts │ │ ├── AutoPlanConfigDto.ts │ │ ├── QbConfigDto.ts │ │ └── BtListItemDto.ts │ ├── bo │ │ └── rules │ │ │ ├── RuleInterface.ts │ │ │ ├── TranslateRole.ts │ │ │ ├── SerializationRule.ts │ │ │ ├── InsertRule.ts │ │ │ ├── DeleteRule.ts │ │ │ ├── AutoRule.ts │ │ │ └── ReplaceRule.ts │ └── vo │ │ ├── RuleObj.ts │ │ └── FileObj.ts ├── desktop │ ├── mac-arm │ │ └── node_sqlite3.node │ ├── mac-x64 │ │ └── node_sqlite3.node │ ├── win-x64 │ │ └── node_sqlite3.node │ ├── linux-arm │ │ └── node_sqlite3.node │ ├── linux-x64 │ │ └── node_sqlite3.node │ └── build.sh ├── READEME.md ├── i18n │ ├── global.yaml │ └── index.ts ├── sqls │ ├── v003_新增默认模板.sql │ ├── v002__init.sql │ ├── v001__init.sql │ ├── v004_新增记录表.sql │ └── v005_新增推荐规则.sql ├── .idea │ ├── vcs.xml │ ├── .gitignore │ ├── modules.xml │ └── openRenamerBackend.iml ├── util │ ├── NumberUtil.ts │ ├── ValUtil.ts │ ├── LogUtil.ts │ ├── pathUtil.ts │ ├── ObjectOperate.ts │ ├── TimeUtil.ts │ ├── ErrorHelper.ts │ ├── NetUtil.ts │ ├── ProcesHelper.ts │ ├── MediaUtil.ts │ ├── SqliteHelper.ts │ ├── QbApiUtil.ts │ └── TranslateUtil.ts ├── api │ ├── PublicApi.ts │ ├── AutoPlanApi.ts │ ├── RenamerApi.ts │ ├── QbServiceApi.ts │ ├── GlobalConfigApi.ts │ ├── ApplicationRuleApi.ts │ └── FileApi.ts ├── tsconfig.json ├── .gitignore ├── service │ ├── GlobalConfigService.ts │ ├── RenamerService.ts │ ├── QbService.ts │ ├── ApplicationRuleService.ts │ ├── AutoPlanService.ts │ └── FileService.ts ├── dao │ ├── SavePathDao.ts │ ├── ApplicationRuleDao.ts │ └── GlobalConfigDao.ts ├── middleware │ ├── handleError.ts │ └── controllerEngine.ts ├── package.json ├── config.ts └── index.ts ├── openRenamerFront ├── src │ ├── config.js │ ├── assets │ │ └── logo.png │ ├── utils │ │ ├── ValUtil.js │ │ ├── Bus.js │ │ └── HttpUtil.js │ ├── components │ │ ├── Tips.vue │ │ ├── rules │ │ │ ├── TranslateRule.vue │ │ │ ├── SerializationRule.vue │ │ │ ├── ReplaceRule.vue │ │ │ ├── AutoRule.vue │ │ │ ├── InsertRule.vue │ │ │ ├── ApplicationRuleList.vue │ │ │ ├── DeleteRule.vue │ │ │ └── RuleBlock.vue │ │ ├── Rule.vue │ │ └── FileChose.vue │ ├── router │ │ └── index.js │ ├── main.js │ ├── i18n │ │ ├── zh.js │ │ └── en.js │ ├── views │ │ ├── public │ │ │ └── login.vue │ │ ├── auto │ │ │ ├── index.vue │ │ │ └── components │ │ │ │ └── editForm.vue │ │ ├── download │ │ │ └── config │ │ │ │ └── index.vue │ │ └── home │ │ │ └── Home.vue │ └── App.vue ├── .browserslistrc ├── .prettierrc ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── vue.config.js ├── jsconfig.json ├── .gitignore ├── README.md └── package.json ├── .DS_Store ├── .vscode └── settings.json ├── Dockerfile ├── .gitignore ├── .idea └── workspace.xml └── README.md /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":1.3 3 | } -------------------------------------------------------------------------------- /openRenamerBackend/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openRenamerFront/src/config.js: -------------------------------------------------------------------------------- 1 | export const version = '1.9.1'; 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/.DS_Store -------------------------------------------------------------------------------- /openRenamerFront/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /openRenamerBackend/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - sqlite3 3 | -------------------------------------------------------------------------------- /openRenamerFront/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 2 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false 4 | } -------------------------------------------------------------------------------- /openRenamerFront/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /openRenamerBackend/start.sh: -------------------------------------------------------------------------------- 1 | pnpm install --registry https://registry.npmmirror.com && tsc && node dist/index.js 2 | -------------------------------------------------------------------------------- /openRenamerFront/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerFront/public/favicon.ico -------------------------------------------------------------------------------- /openRenamerFront/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerFront/src/assets/logo.png -------------------------------------------------------------------------------- /openRenamerBackend/entity/constants/GlobalConfigCodeConstant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 默认模板id 3 | */ 4 | export const DEFAULT_TEMPLETE_ID = "defaultTempleteId"; -------------------------------------------------------------------------------- /openRenamerBackend/desktop/mac-arm/node_sqlite3.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerBackend/desktop/mac-arm/node_sqlite3.node -------------------------------------------------------------------------------- /openRenamerBackend/desktop/mac-x64/node_sqlite3.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerBackend/desktop/mac-x64/node_sqlite3.node -------------------------------------------------------------------------------- /openRenamerBackend/desktop/win-x64/node_sqlite3.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerBackend/desktop/win-x64/node_sqlite3.node -------------------------------------------------------------------------------- /openRenamerBackend/READEME.md: -------------------------------------------------------------------------------- 1 | bba# 客户端打包 2 | **node版本要求18.xx** 3 | 1. 全局安装typescript,pkg 4 | ```bash 5 | pnpm -g typescript pkg 6 | ``` 7 | 2. 执行desktop/build.sh脚本 -------------------------------------------------------------------------------- /openRenamerBackend/desktop/linux-arm/node_sqlite3.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerBackend/desktop/linux-arm/node_sqlite3.node -------------------------------------------------------------------------------- /openRenamerBackend/desktop/linux-x64/node_sqlite3.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FleyX/open-renamer/HEAD/openRenamerBackend/desktop/linux-x64/node_sqlite3.node -------------------------------------------------------------------------------- /openRenamerFront/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: "http://localhost:8089", 4 | }, 5 | publicPath: "./" 6 | }; 7 | -------------------------------------------------------------------------------- /openRenamerBackend/i18n/global.yaml: -------------------------------------------------------------------------------- 1 | key-error: 2 | en: Secret error 3 | zh: 秘钥错误 4 | qb: 5 | version-error: 6 | en: qBittorrent version must be V4.xx 7 | zh: qBittorrent 版本必须为V4.xx -------------------------------------------------------------------------------- /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/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/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/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /openRenamerBackend/entity/dto/QbCommonDto.ts: -------------------------------------------------------------------------------- 1 | export default interface QbCommonDto { 2 | method: string; 3 | url: string; 4 | query: any; 5 | body: any; 6 | /** 7 | * 1:application/json 2:formdata 3:application/x-www-form-urlencoded 8 | */ 9 | contentType: number; 10 | } -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:hydrogen-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 | -------------------------------------------------------------------------------- /openRenamerBackend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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/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/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/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 | ); -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 18 | 19 | app 20 | app.exe 21 | desktop/**/*.zip 22 | desktop/**/renamer-* 23 | desktop/**/*-desktop 24 | desktop/**/data/** 25 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /openRenamerFront/src/components/Tips.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /openRenamerBackend/.idea/openRenamerBackend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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-i18n": "9.5.0", 18 | "vue-router": "4.2.5" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "5.0.8", 22 | "@vue/cli-plugin-router": "5.0.8", 23 | "@vue/cli-service": "5.0.8", 24 | "@vue/compiler-sfc": "3.0.0", 25 | "less": "3.0.4", 26 | "less-loader": "5.0.0", 27 | "prettier": "2.2.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 { createI18n } from "vue-i18n"; 6 | import "element-plus/dist/index.css"; 7 | 8 | import en from "./i18n/en"; 9 | import zh from "./i18n/zh"; 10 | 11 | const messages = { 12 | en, 13 | zh 14 | }; 15 | 16 | 17 | const lang = (navigator.language || "en").toLocaleLowerCase(); 18 | const locale = localStorage.getItem("lang") || lang.split("-")[0] || "en"; 19 | console.log(messages, locale); 20 | const i18n = createI18n({ 21 | legacy: false, 22 | locale, 23 | fallbackLocale: "en", 24 | messages 25 | }); 26 | 27 | const vueInstance = createApp(App); 28 | vueInstance.use(router).use(ElementPlus).use(i18n).mount("#app"); 29 | vueInstance.config.globalProperties.$message = ElMessage; 30 | window.vueInstance = vueInstance; 31 | -------------------------------------------------------------------------------- /openRenamerFront/src/i18n/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | langChange: "选择语言", 3 | version: "版本", 4 | newestVersion: "最新版本", 5 | openSourceLocation: "开源地址", 6 | feedback: "反馈", 7 | menu: { 8 | rename: "重命名", 9 | download: "qBittorrent下载", 10 | downloadConfig: "配置", 11 | downloadHome: "下载中心" 12 | }, 13 | action: { 14 | success: "操作成功", 15 | fail: "操作失败", 16 | edit: "编辑", 17 | cancel: "取消", 18 | submit: "提交", 19 | confirm: "确认" 20 | }, 21 | qbConfig: { 22 | address: "部署地址", 23 | addressAlt: "例如:http://localhost:8080(仅支持v4.1及以上)", 24 | error: "配置错误", 25 | version: "版本", 26 | username: "用户名", 27 | usernameAlt: "qBittorrent 用户名", 28 | password: "密码", 29 | passwordAlt: "qBittorrent 密码", 30 | downloadPath: "qb下载路径", 31 | downloadPathAlt: "qb下载路径(如果qbittorrent和open-renamer部署在不同的docker容器中,需要配置此项)", 32 | localPath: "对应本系统路径", 33 | localPathAlt: "对应本系统路径(如果qbittorrent和open-renamer部署在不同的docker容器中,需要配置此项)" 34 | } 35 | }; -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /openRenamerFront/src/views/public/login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 41 | -------------------------------------------------------------------------------- /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/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'yaml'; 2 | import fs from 'fs-extra'; 3 | import path from 'path' 4 | import config from '../config' 5 | import logger from '../util/LogUtil'; 6 | 7 | const map = {}; 8 | 9 | export function init() { 10 | let i18nFolder = path.join(config.rootPath, 'i18n'); 11 | let files = fs.readdirSync(i18nFolder).filter(item => item.endsWith(".yaml")); 12 | files.forEach(file => { 13 | let res = YAML.parse(fs.readFileSync(path.join(i18nFolder, file), 'utf-8')); 14 | dealYaml("", res); 15 | }) 16 | logger.info("i18n加载完毕"); 17 | } 18 | 19 | export function getMessage(lang: string, key: string): string { 20 | let val = map[key + "." + (lang ? lang : 'en')]; 21 | return val ? val : key; 22 | } 23 | 24 | function dealYaml(pre: string, res: any) { 25 | Object.keys(res).forEach(key => { 26 | let val = res[key]; 27 | let mapKey = pre == '' ? key : (pre + "." + key); 28 | if (typeof val != "object") { 29 | map[mapKey] = val; 30 | } else { 31 | dealYaml(mapKey, val); 32 | } 33 | }) 34 | } -------------------------------------------------------------------------------- /openRenamerBackend/middleware/handleError.ts: -------------------------------------------------------------------------------- 1 | import log from '../util/LogUtil'; 2 | import config from "../config"; 3 | import {getMessage} from "../i18n"; 4 | 5 | let f = async (ctx, next) => { 6 | let lang = ctx.request.headers['lang']; 7 | try { 8 | //检查是否有密码 9 | if (checkToken(ctx)) { 10 | await next(); 11 | } else { 12 | ctx.status = 401; 13 | ctx.body = getMessage(lang, "key-error"); 14 | } 15 | } catch (error: any) { 16 | if (error.status != undefined) { 17 | ctx.status = error.status; 18 | } else { 19 | ctx.status = 500; 20 | } 21 | ctx.body = getMessage(lang, error.message); 22 | log.error(error); 23 | } 24 | } 25 | 26 | function checkToken(ctx) { 27 | if (!config.token) { 28 | return true; 29 | } 30 | let requestPath = ctx.method + ctx.path.replace(config.urlPrefix, ""); 31 | if (config.publicPath.has(requestPath)) { 32 | return true; 33 | } 34 | return config.token == ctx.headers.token; 35 | 36 | } 37 | 38 | export default f; -------------------------------------------------------------------------------- /openRenamerFront/src/views/auto/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | 56 | -------------------------------------------------------------------------------- /openRenamerBackend/util/NetUtil.ts: -------------------------------------------------------------------------------- 1 | import NumberUtil from './NumberUtil' 2 | import {execSync} from 'child_process'; 3 | import * as process from "process"; 4 | 5 | export function getPort(start, end): number { 6 | let count = 100; 7 | while (count-- > 0) { 8 | let num = NumberUtil.getRandom(start, end); 9 | if (checkFree(num)) { 10 | return num; 11 | } 12 | } 13 | throw new Error("无可用端口"); 14 | } 15 | 16 | export function checkFree(port: number): boolean { 17 | let stdout = null 18 | let platform = process.platform.toLocaleLowerCase(); 19 | try { 20 | if (platform.includes("win32")) { 21 | //windows 22 | stdout = execSync(`netstat -ano | findstr :${port}`); 23 | } else if (platform.includes('darwin')) { 24 | //mac 25 | stdout = execSync(`lsof -i:${port}`); 26 | } else { 27 | //Linux 28 | stdout = execSync(`netstat -tulpn | grep :${port}`); 29 | if (stdout.includes("command not found") || stdout.includes("未找到命令")) { 30 | stdout = execSync(`ss -tulpn | grep :${port}`); 31 | } 32 | } 33 | console.log(stdout); 34 | } catch (e) { 35 | return true; 36 | } 37 | return !stdout; 38 | } -------------------------------------------------------------------------------- /openRenamerBackend/util/ProcesHelper.ts: -------------------------------------------------------------------------------- 1 | import * as childPrecess from 'child_process'; 2 | import logUtil from "./LogUtil"; 3 | import logger from "./LogUtil"; 4 | import config from "../config"; 5 | 6 | class ProcessHelper { 7 | static exec(cmd): Promise { 8 | return new Promise((resolve, reject) => { 9 | childPrecess.exec(cmd, (error, stdout, stderr) => { 10 | if (error) { 11 | reject(error); 12 | } 13 | if (stderr) { 14 | reject(stderr); 15 | } else { 16 | resolve(stdout) 17 | } 18 | }) 19 | }) 20 | } 21 | 22 | static kill(pid: number): void { 23 | try { 24 | if(config.isWindows){ 25 | childPrecess.execSync("taskkill /pid " + pid) 26 | }else{ 27 | childPrecess.execSync("kill " + pid) 28 | } 29 | } catch (e) { 30 | logger.info("进程kill报错:" + (e as Error).message); 31 | } 32 | } 33 | } 34 | 35 | 36 | // (async()=>{ 37 | // let res= await ProcessHelper.exec('cd /d e://workspace&&dir'); 38 | // console.log(res); 39 | // })() 40 | 41 | export default ProcessHelper -------------------------------------------------------------------------------- /openRenamerBackend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nas_backup", 3 | "version": "1.0.0", 4 | "description": "文件备份用", 5 | "main": "index.js", 6 | "scripts": { 7 | "pkg-mac-arm": "tsc && pkg . -t macos-arm64 -o desktop/mac-arm/renamer-mac", 8 | "pkg-mac-x64": "tsc && pkg . -t macos-x64 -o desktop/mac-x64/renamer-mac", 9 | "pkg-win-x64": "tsc && pkg . -t win-x64 -o desktop/win-x64/renamer-win", 10 | "pkg-linux-arm": "tsc && pkg . -t linux-arm64 -o desktop/linux-arm/renamer-linux", 11 | "pkg-linux-x64": "tsc && pkg . -t linux-x64 -o desktop/linux-x64/renamer-linux" 12 | }, 13 | "bin": "dist/index.js", 14 | "pkg": { 15 | "assets": [ 16 | "dist/**/*", 17 | "static/**/**", 18 | "sqls/*", 19 | "i18n/*" 20 | ] 21 | }, 22 | "author": "fxb", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@types/fs-extra": "5.0.4", 26 | "@types/koa": "2.13.12", 27 | "@types/node": "11.13.4", 28 | "axios": "0.29.0", 29 | "fs-extra": "7.0.0", 30 | "koa": "3.0.0", 31 | "koa-body": "4.0.4", 32 | "koa-router": "13.0.1", 33 | "koa-static": "5.0.0", 34 | "koa2-cors": "2.0.6", 35 | "log4js": "6.4.0", 36 | "moment": "2.29.4", 37 | "sqlite": "5.1.1", 38 | "sqlite3": "5.1.5", 39 | "uuid": "3.3.2", 40 | "yaml": "2.7.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /openRenamerFront/src/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | langChange: "change language", 3 | version: "version", 4 | newestVersion: "latest version", 5 | openSourceLocation: "Open source address", 6 | feedback: "Feedback", 7 | menu: { 8 | rename: "Rename", 9 | download: "QBittorrent Download", 10 | downloadConfig: "Config", 11 | downloadHome: "Download center" 12 | }, 13 | action: { 14 | success: "action success", 15 | fail: "action fail", 16 | edit: "edit", 17 | cancel: "cancel", 18 | submit: "submit", 19 | confirm: "confirm" 20 | }, 21 | qbConfig: { 22 | address: "Deploy address", 23 | addressAlt: "Example: http://localhost:8080 (only v4.1 and above)", 24 | error: "Error", 25 | version: "Version", 26 | username: "Username", 27 | usernameAlt: "QBittorrent username", 28 | password: "Password", 29 | passwordAlt: "QBittorrent password", 30 | downloadPath: "QB download path", 31 | downloadPathAlt: "qBittorrent download path (this will need to be configured if qbittorrent and open-renamer are deployed in different docker containers)", 32 | localPath: "Local path", 33 | localPathAlt: "Corresponding to the path of the system (if qbittorrent and open-renamer are deployed in different docker containers, this parameter needs to be configured)" 34 | } 35 | }; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as process from "process"; 3 | import {getPort} from './util/NetUtil'; 4 | 5 | //后台所在绝对路径 6 | const rootPath = path.resolve(__dirname, '..'); 7 | let map = {}; 8 | console.log(process.argv); 9 | //argv 传递 port,dataPath,env,token 10 | for (let i = 2; i < process.argv.length; i++) { 11 | if (process.argv[i] != null && process.argv[i] != '') { 12 | let strings = process.argv[i].split(":"); 13 | map[strings[0]] = strings[1]; 14 | } 15 | } 16 | //dev,prod,desktop 17 | let env = map['env'] ? map['env'] : process.env.ENV ? process.env.ENV : "dev"; 18 | let basePort = map['port'] ? parseInt(map['port']) : process.env.PORT ? parseInt(process.env.PORT) : 8089; 19 | 20 | let config = { 21 | rootPath, 22 | dataPath: map['dataPath'] ? map['dataPath'] : process.env.DATA_PATH ? process.env.DATA_PATH : 23 | env == 'desktop' ? path.join(process.argv[0], "..", 'data') : path.join(rootPath, 'data'), 24 | port: env == 'desktop' ? getPort(20000, 50000) : basePort, 25 | token: map['token'] ? map['token'] : process.env.TOKEN ? process.env.TOKEN : null, 26 | env, 27 | urlPrefix: '/openRenamer/api', 28 | //是否为windows平台 29 | isWindows: process.platform.toLocaleLowerCase().includes("win32"), 30 | isMac: process.platform.toLocaleLowerCase().includes("darwin"), 31 | bodyLimit: { 32 | formLimit: '200mb', 33 | jsonLimit: '200mb', 34 | urlencoded: true, 35 | multipart: true, 36 | formidable: { 37 | uploadDir: path.join(rootPath, 'files', 'temp', 'uploads'), 38 | keepExtenstions: true, 39 | maxFieldsSize: 1024 * 1024 * 200 40 | } 41 | }, 42 | publicPath: new Set(["POST/public/checkToken"]) 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /openRenamerBackend/desktop/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | script_abs=$(readlink -f "$0") 3 | script_dir=$(dirname "${script_abs}") 4 | # shellcheck disable=SC2164 5 | cd "$script_dir"/../../openRenamerFront 6 | npm run build 7 | cp -r dist/* ../openRenamerBackend/static 8 | # shellcheck disable=SC2164 9 | cd ../openRenamerBackend 10 | 11 | # "dev"替换为desktop 12 | sed -i 's/"dev"/"desktop"/g' config.ts 13 | 14 | 15 | # mac-arm-arm打包 16 | npm run pkg-mac-arm 17 | # 打zip包 18 | # shellcheck disable=SC2164 19 | cd ./desktop/mac-arm 20 | zip renamer-mac-arm-desktop.zip renamer-mac mac-start.command 21 | echo mac-arm over 22 | # 解压后,首次运行会因为mac安全检查导致无法启动,解决步骤如下: 23 | # 1. 双击renamer-mac启动应用 24 | # 2. 此时会弹窗未打开“renamer-mac-arm" Apple无法验证。。。。。。 点击完成按钮关闭告警 25 | # 3. 然后进入设置-隐私与安全性-安全性 点击仍要打开 弹窗选择仍要打开 完成设置 26 | # 4. 双击mac-start.command启动脚本(此脚本会自动打开浏览器页面) 27 | # 5. 重复2-3步骤 28 | 29 | 30 | # mac-arm-x64打包 31 | cd ../../ 32 | npm run pkg-mac-x64 33 | # 打zip包 34 | # shellcheck disable=SC2164 35 | cd ./desktop/mac-x64 36 | zip renamer-mac-x64-desktop.zip renamer-mac mac-start.command 37 | echo mac-x64 over 38 | 39 | 40 | # windows-x64打包 41 | cd ../../ 42 | npm run pkg-win-x64 43 | # 打zip包 44 | # shellcheck disable=SC2164 45 | cd ./desktop/win-x64 46 | zip renamer-win-x64-desktop.zip renamer-win.exe win-start.bat node_sqlite3.node 47 | echo win-x64 over 48 | 49 | 50 | # linux-x64打包 51 | cd ../../ 52 | npm run pkg-linux-x64 53 | # 打zip包 54 | # shellcheck disable=SC2164 55 | cd ./desktop/linux-x64 56 | zip renamer-linux-x64-desktop.zip renamer-linux linux-start.sh node_sqlite3.node 57 | echo linux-x64 over 58 | 59 | 60 | # linux-arm打包 61 | cd ../../ 62 | npm run pkg-linux-arm 63 | # 打zip包 64 | # shellcheck disable=SC2164 65 | cd ./desktop/linux-arm 66 | zip renamer-linux-arm-desktop.zip renamer-linux linux-start.sh node_sqlite3.node 67 | echo linux-arm over 68 | 69 | # 替换回去 70 | cd ../../ 71 | sed -i 's/"desktop"/"dev"/g' config.ts 72 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/TranslateRule.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 59 | 60 | 78 | -------------------------------------------------------------------------------- /openRenamerBackend/index.ts: -------------------------------------------------------------------------------- 1 | import fs, {writeFileSync, readFileSync, pathExistsSync} from 'fs-extra'; 2 | import koa from "koa"; 3 | import Router from "koa-router"; 4 | import koaBody from "koa-body"; 5 | import * as path from "path"; 6 | import RouterMW from "./middleware/controllerEngine"; 7 | 8 | import config from "./config"; 9 | import handleError from "./middleware/handleError"; 10 | import SqliteUtil from './util/SqliteHelper'; 11 | import log from './util/LogUtil'; 12 | import qbService from "./service/QbService"; 13 | import * as i18n from './i18n'; 14 | import * as process from "process"; 15 | import ProcesHelper from "./util/ProcesHelper"; 16 | import {execSync} from 'child_process'; 17 | 18 | let start = Date.now(); 19 | console.log(config); 20 | 21 | const app = new koa(); 22 | 23 | let router = new Router({ 24 | prefix: config.urlPrefix 25 | }); 26 | 27 | app.use(require('koa-static')(path.join(config.rootPath, 'static'))); 28 | 29 | //表单解析 30 | app.use(koaBody(config.bodyLimit)); 31 | //错误处理 32 | app.use(handleError); 33 | 34 | app.use(RouterMW(router, path.join(config.rootPath, "dist/api"))); 35 | (async () => { 36 | let pidPath = path.join(config.dataPath, 'pid'); 37 | //尝试杀死历史进程 38 | if (pathExistsSync(pidPath)) { 39 | let pid = readFileSync(pidPath, 'utf-8'); 40 | ProcesHelper.kill(parseInt(pid)); 41 | } 42 | await SqliteUtil.createPool(); 43 | await qbService.init(); 44 | i18n.init(); 45 | app.listen(config.port); 46 | log.info(`server listened ${config.port},cost:${Date.now() - start}ms`); 47 | //写启动端口 48 | writeFileSync(path.join(config.dataPath, 'port'), config.port.toString()); 49 | //写进程号 50 | writeFileSync(pidPath, process.pid.toString()); 51 | //如果为桌面环境,打开浏览器 52 | if (config.env == 'desktop') { 53 | await openBrowser(`http://localhost:${config.port}`); 54 | } 55 | })(); 56 | 57 | app.on("error", (error) => { 58 | console.error(error); 59 | }) 60 | 61 | 62 | async function openBrowser(url: string) { 63 | execSync(config.isWindows ? `start "" "${url}"` : config.isMac ? `open '${url}'` : `xdg-open ${url}`); 64 | } 65 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /openRenamerBackend/service/QbService.ts: -------------------------------------------------------------------------------- 1 | import QbConfigDto from "../entity/dto/QbConfigDto"; 2 | import {get, getQbInfo, post, tryLogin, updateQbInfo} from '../util/QbApiUtil'; 3 | import GlobalConfigService from "./GlobalConfigService"; 4 | import GlobalConfig from "../entity/po/GlobalConfig"; 5 | import BtListItemDto from "../entity/dto/BtListItemDto"; 6 | import QbCommonDto from "../entity/dto/QbCommonDto"; 7 | import logger from "../util/LogUtil"; 8 | 9 | class QbService { 10 | 11 | /** 12 | * 保存地址 13 | * @param body 14 | */ 15 | static async saveAddress(body: QbConfigDto): Promise { 16 | if (body.address.endsWith("/")) { 17 | body.address = body.address.substring(0, body.address.length - 1); 18 | } 19 | updateQbInfo(body); 20 | body.valid = await tryLogin(); 21 | body.version = body ? (await get("/app/version", null)) : null; 22 | if (parseFloat(body.version.replace("v", "")) < 4.1) { 23 | body.valid = false; 24 | body.version = null; 25 | throw new Error("qb.version-error"); 26 | } 27 | await GlobalConfigService.insertOrReplace(new GlobalConfig("qbConfig", JSON.stringify(body), "qb config")); 28 | return body; 29 | } 30 | 31 | /** 32 | * 获取当前配置 33 | */ 34 | static async getConfig(): Promise { 35 | return getQbInfo(); 36 | } 37 | 38 | /** 39 | * get torrents list from qb 40 | */ 41 | static async getBtList(): Promise> { 42 | let res = await get("/api/v2/torrents/info?category=&sort=added_on", null); 43 | return res; 44 | } 45 | 46 | static async commonGet(body: QbCommonDto): Promise { 47 | return await get(body.url, body.body); 48 | } 49 | 50 | 51 | static async commonPost(body: QbCommonDto): Promise { 52 | return await post(body.url, body.query, body.body, body.contentType); 53 | } 54 | 55 | 56 | /** 57 | * 初始化 58 | */ 59 | static async init() { 60 | let config = await GlobalConfigService.getVal("qbConfig"); 61 | let qbInfo: QbConfigDto = config == null ? {} : JSON.parse(config); 62 | updateQbInfo(qbInfo); 63 | qbInfo.valid = await tryLogin(); 64 | qbInfo.version = qbInfo.valid ? (await get("/app/version", null)) : null; 65 | } 66 | } 67 | 68 | export default QbService; 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, lang: localStorage.getItem("lang") } 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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 13 | 14 | 16 | 17 | 19 | 22 | 23 | 24 | 27 | 43 | 44 | 45 | 46 | 47 | 1743248633691 48 | 53 | 54 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/download/config/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 61 | 62 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/Rule.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 62 | 63 | 75 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/SerializationRule.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 87 | 88 | 106 | -------------------------------------------------------------------------------- /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, 1); 20 | } 21 | 22 | /** 23 | * 24 | * @param url 25 | * @param data 26 | * @param formType 1:application/json 2:formdata 3:application/x-www-form-urlencoded 27 | */ 28 | export async function post(url: string, query: any, data: object, formType = 1) { 29 | return await request("post", url, query, data, formType); 30 | } 31 | 32 | /** 33 | * 34 | * @param url 35 | * @param data 36 | * @param formType 1:application/json 2:formdata 3:application/x-www-form-urlencoded 37 | */ 38 | async function request(method: Method, url: string, query: any, body: any, formType = 1) { 39 | if (!qbInfo.valid) { 40 | throw new Error("qbittorrent无法连接,请检查配置"); 41 | } 42 | let isTryLogin = false; 43 | while (true) { 44 | let headers = {"Cookie": cookie}; 45 | if (method == 'post') { 46 | if (formType == 1) { 47 | headers['content-type'] = "application/json"; 48 | } else if (formType == 2) { 49 | headers['content-type'] = "multipart/form-data"; 50 | } else { 51 | headers['content-type'] = "application/x-www-form-urlencoded"; 52 | } 53 | } 54 | let res = await axios.request({ 55 | baseURL: qbInfo.address, 56 | url: "/api/v2" + url, 57 | method, 58 | params: query, 59 | data: body, 60 | headers, 61 | }); 62 | if (res.status == 200) { 63 | return res.data; 64 | } 65 | if (res.status == 403) { 66 | if (isTryLogin) { 67 | throw new Error("qb用户名密码设置有误"); 68 | } else { 69 | await tryLogin(); 70 | isTryLogin = true; 71 | } 72 | } else { 73 | throw new Error("请求报错:" + res.data); 74 | } 75 | } 76 | 77 | } 78 | 79 | export async function tryLogin(): Promise { 80 | if (qbInfo == null || qbInfo.address == null || qbInfo.address == "") { 81 | return false; 82 | } 83 | let body = {username: qbInfo.username, password: qbInfo.password}; 84 | try { 85 | let res = await axios.post(qbInfo.address + `/api/v2/auth/login`, querystring.stringify(body), { 86 | headers: {"Content-Type": "application/x-www-form-urlencoded"} 87 | }); 88 | let success = res.data.toLocaleLowerCase().indexOf('ok') > -1; 89 | if (success) { 90 | cookie = res.headers['set-cookie']; 91 | } 92 | qbInfo.valid = success; 93 | return success; 94 | } catch (error:any) { 95 | console.error("qb登录报错:" ); 96 | return false; 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/ReplaceRule.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 102 | 103 | 121 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/AutoRule.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 97 | 98 | 116 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/InsertRule.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 98 | 99 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-renamer 2 | 3 | ![预览图](https://s3.fleyx.com/picbed/2022/11/18386180128d01eb1a59b8eacf652895.png) 4 | 5 | renamer 的开源实现版本,BS 应用,支持全平台部署使用 6 | 7 | - 服务器部署,支持 docker,已打包镜像到 dockerhub 中:[hub.docker.com](https://hub.docker.com/r/fleyx/open-renamer),可直接使用。 8 | - 客户端直接启动,github release 页中下载对应平台客户端(mac,linux,windows),[点击去下载](https://github.com/FleyX/open-renamer/releases) 9 | renamer 的开源实现版本,BS 应用,两种使用方式支持全平台部署使用 10 | 11 | 开源地址:[github.com/FleyX/open-renamer](https://github.com/FleyX/open-renamer) 12 | 13 | 已实现如下处理规则 : 14 | 15 | - 插入(支持季号识别,支持后缀过滤) 16 | - 删除(支持正则) 17 | - 替换(支持正则) 18 | - 序列化 19 | - 自动识别(针对 nas 用户开发,自动获取季号,剧名/电影名) 20 | - 简繁转换(支持港澳繁体,台湾繁体) 21 | 22 | 特点: 23 | 24 | - 不限制规则数量 25 | - 支持将规则保存为模板,方便下次使用 26 | - 全平台(arm,x86)支持,可直接部署在 nas 中,通过浏览器访问;也可直接下载客户端应用本地启动 27 | - 针对 nas 视频文件有特殊优化,智能识别剧集名称,季号,集数,方便 jellyfin、emby 等软件识别 28 | 29 | ## 客户端安装 30 | 31 | 跳转[github.com/FleyX/open-renamer/releases/latest](https://github.com/FleyX/open-renamer/releases/latest) 下载对应平台的 zip 压缩包,解压后执行 32 | 33 | ## docker 部署 34 | 35 | **非必须,建议直接下载可执行文件本地运行** 36 | 37 | ### 首次部署 38 | 39 | 推荐通过 docker 部署到 nas 中,即可管理 nas 媒体文件 40 | 41 | #### docker 部署 42 | 43 | ```bash 44 | # 管理/mnt/vdisk目录中的文件,通过8089端口访问服务 45 | docker run -itd --name openRenamer -v /mnt/vdisk:/data -p 8089:8089 -e PORT="8089" -e TOKEN="123456" fleyx/open-renamer:latest 46 | ``` 47 | 48 | - docker-compose 运行: 49 | 50 | ```yaml 51 | version: "3.6" 52 | openRenamer: 53 | container_name: openRenamer 54 | image: fleyx/open-renamer:latest 55 | # 当前用户的uid,gid,使用root可不配置此项 56 | #user: "1000:1000" 57 | environment: 58 | # 指定启动端口 59 | - PORT=11004 60 | # 指定认证token,不设置此项无需认证 61 | - TOKEN=123456 62 | volumes: 63 | # 关键,把想要管理的文件夹映射到容器的data目录中,即可在程序中选择data目录进行重命名操作 64 | - /mnt/vdisk:/data 65 | # 存储模板数据,可不映射此目录 66 | - ./data/openRenamer:/app/data 67 | # 使用宿主机网络.即可通过"宿主机ip:11004"访问程序 68 | network_mode: host 69 | ``` 70 | 71 | #### 代码部署 72 | 73 | 1. 安装最新的 node 环境 74 | 2. 下载代码 75 | 3. 编译前后端 76 | 4. 将前端打包后 dist 目录下所有的文件复制到后端的 static 目录下 77 | 78 | ### 升级 79 | 80 | 1. 如果使用 latest 版本,通过`docker pull fleyx/open-renamer:latest`命令更新镜像 81 | 2. 如果使用版本号,直接修改 docker 版本号为最新的版本号,重新运行即可 82 | 83 | ## TODO 84 | 85 | ## 版本更新记录 86 | 87 | 88 | ### 1.9.1 89 | 90 | 客户端应用优化,去除启动脚本,直接双击应用启动 91 | 92 | ### 1.9.0 93 | 94 | 新增 windows,linux,mac 客户端,根据各自平台选择对应客户端 95 | 96 | - **linux x64**: renamer-linux-x64-desktop.zip 97 | - **linux arm64**: renamer-linux-arm-desktop.zip 98 | - **windows x64**: renamer-win-x64-desktop.zip 99 | - **mac arm**: renamer-mac-arm-desktop.zip 100 | - **mac intel**: renamer-mac-x64-desktop.zip 101 | 102 | ### 1.8.0 103 | 104 | - 删除、替换规则支持正则表达式 105 | - 修复删除规则的一些问题 106 | 107 | ### 1.7.1 108 | 109 | - 新增简繁转换规则 [issue 38](https://github.com/FleyX/open-renamer/issues/38) 110 | - 全选支持文件夹 [issue 39](https://github.com/FleyX/open-renamer/issues/39) 111 | - 客户端打开增加 loading 效果 112 | 113 | ### 1.7 114 | 115 | - 通过 electron 技术支持桌面端,目前已支持 windows 116 | 117 | ### 1.6.2 118 | 119 | - 修复文件数量过多时报错 bug [#35](https://github.com/FleyX/open-renamer/issues/35) 120 | 121 | ### 1.6.1 122 | 123 | - 文本过长显示 bug [#34](https://github.com/FleyX/open-renamer/issues/34) 124 | ![修复文本过长显示](https://s3.fleyx.com/picbed/2023/05/4374cc1b43bfe1c670434317baeaf389.png) 125 | 126 | ### 1.6 127 | 128 | - 新增替换规则 [#33](https://github.com/FleyX/open-renamer/issues/33) 129 | 130 | ![替换规则](https://s3.fleyx.com/picbed/2023/05/f94d2a2579f728a5ff478f046ca4786e.png) 131 | 132 | - 修复一些 bug 133 | 134 | ### 1.5 135 | 136 | - 修复模板保存 bug[#31](https://github.com/FleyX/open-renamer/issues/31) 137 | - 季识别优化,支持中文(一,二,三。。。十),最大支持九十九 [#29](https://github.com/FleyX/open-renamer/issues/29) 138 | - 序列化规则支持根据后缀分组[#30](https://github.com/FleyX/open-renamer/issues/30) 139 | 140 | ### 1.4 141 | 142 | - 季识别优化,增加 season.01,文本 01 类型支持 143 | 144 | ### 1.3 145 | 146 | - 添加文件支持勾选文件夹,以添加文件夹下所有内容 147 | ![1](https://s3.fleyx.com/picbed/2023/03/bc3ee7aadf8fd2f3bfc1381b92b4bd89.png) 148 | - 支持文件列表下直接重命名、删除文件 149 | ![1](https://s3.fleyx.com/picbed/2023/03/24f29ad19885c3b8cff93be2b2f6e508.png) 150 | ![1](https://s3.fleyx.com/picbed/2023/03/0fe66150fc15b34e7005c74e3604eb48.png) 151 | - 支持一键选中非视频、字幕、nfo 文件,方便清理 152 | - 添加剧集重命名参考模板 153 | 154 | **注意本次更新后将会通过接口获取用户使用情况(不会获取任何隐私数据)** 155 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/ApplicationRuleList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 118 | 119 | 122 | -------------------------------------------------------------------------------- /openRenamerFront/src/App.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 102 | 103 | 159 | -------------------------------------------------------------------------------- /openRenamerFront/src/views/auto/components/editForm.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 148 | 149 | 165 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/DeleteRule.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 151 | 152 | 176 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/rules/RuleBlock.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 189 | 190 | 215 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /openRenamerFront/src/components/FileChose.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 178 | 179 | 199 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /openRenamerFront/src/views/home/Home.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 363 | 364 | 409 | --------------------------------------------------------------------------------