Merge branch 'dev' of ssh://gitea.fleyx.com:222/fanxb/open-renamer into dev
# Conflicts: # openRenamerBackend/api/AutoPlanApi.ts
This commit is contained in:
commit
d52398f14c
14
openRenamerBackend/api/AutoPlanApi.ts
Normal file
14
openRenamerBackend/api/AutoPlanApi.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Context } from "koa";
|
||||
import AutoPlanService from "../service/AutoPlanService";
|
||||
|
||||
const router = {};
|
||||
|
||||
/**
|
||||
* 获取目录下的文件列表
|
||||
*/
|
||||
router["POST /autoPlan/save"] = async function (ctx: Context) {
|
||||
ctx.body = await AutoPlanService.saveAutoConfig(ctx.request.body);
|
||||
};
|
||||
|
||||
|
||||
export default router;
|
@ -4,6 +4,7 @@ import GlobalConfigDao from '../dao/GlobalConfigDao';
|
||||
|
||||
import { DEFAULT_TEMPLETE_ID } from '../entity/constants/GlobalConfigCodeConstant';
|
||||
import GlobalConfig from '../entity/po/GlobalConfig';
|
||||
import ErrorHelper from '../util/ErrorHelper';
|
||||
|
||||
|
||||
class ApplicationRuleService {
|
||||
@ -25,6 +26,11 @@ class ApplicationRuleService {
|
||||
}
|
||||
|
||||
static async deleteById(id: number): Promise<void> {
|
||||
//禁止删除默认模板
|
||||
let idStr = await GlobalConfigDao.getByCode(DEFAULT_TEMPLETE_ID);
|
||||
if (id.toString() === idStr) {
|
||||
throw ErrorHelper.Error400("禁止删除默认模板");
|
||||
}
|
||||
await ApplicationRuleDao.delete(id);
|
||||
}
|
||||
|
||||
|
@ -2,16 +2,15 @@ import config from '../config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import ProcessHelper from '../util/ProcesHelper';
|
||||
import FileObj from '../entity/vo/FileObj';
|
||||
import SavePathDao from '../dao/SavePathDao';
|
||||
import SavePath from '../entity/po/SavePath';
|
||||
import AutoPlanConfigDto from '../entity/dto/AutoPlanConfigDto';
|
||||
import GlobalConfig from 'entity/po/GlobalConfig';
|
||||
import GlobalConfigService from './GlobalConfigService';
|
||||
import ErrorHelper from 'util/ErrorHelper';
|
||||
|
||||
import ErrorHelper from '../util/ErrorHelper';
|
||||
import TimeUtil from '../util/TimeUtil';
|
||||
import { isSub, isVideo } from '../util/MediaUtil';
|
||||
import log from '../util/LogUtil';
|
||||
const autoConfigCode = "autoConfig";
|
||||
let isReadDir = false;
|
||||
/**
|
||||
* 需要处理的文件
|
||||
*/
|
||||
@ -34,12 +33,25 @@ class AutoPlanService {
|
||||
} else {
|
||||
autoConfig = JSON.parse(str);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
await TimeUtil.sleep(1000);
|
||||
await work();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
static async saveAutoConfig(body: AutoPlanConfigDto): Promise<void> {
|
||||
if (isReadDir) {
|
||||
throw ErrorHelper.Error400("正在处理中,请稍后再试");
|
||||
}
|
||||
if (body.start) {
|
||||
if (body.paths.length == 0) {
|
||||
throw ErrorHelper.Error400("视频路径为空");
|
||||
@ -56,28 +68,48 @@ class AutoPlanService {
|
||||
await GlobalConfigService.insertOrReplace(configBody);
|
||||
autoConfig = body;
|
||||
if (body.start && !body.ignoreExist) {
|
||||
setTimeout(async () => {
|
||||
isReadDir = true;
|
||||
try {
|
||||
await readDir(body.paths);
|
||||
} finally {
|
||||
isReadDir = false;
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取目录,获取文件列表
|
||||
* @param dirList 要读取的目录
|
||||
*/
|
||||
async function readDir(dirList: Array<string>): Promise<void> {
|
||||
if (!dirList) {
|
||||
return;
|
||||
}
|
||||
for (let i in dirList) {
|
||||
let pathStr = dirList[i];
|
||||
if (checkIgnore(pathStr)) {
|
||||
if (checkIgnore(path.basename(pathStr))) {
|
||||
continue;
|
||||
}
|
||||
if (!(await fs.stat(pathStr)).isDirectory()) {
|
||||
let fileName = path.basename(pathStr);
|
||||
let strs = fileName.split('.').reverse();
|
||||
if (strs.length > 0 && (isSub(strs[0]) || isVideo(strs[1]))) {
|
||||
needDeal.push(pathStr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await readDir((await fs.readdir(pathStr)).map(item => path.join(pathStr, item)));
|
||||
let childs = null;
|
||||
try {
|
||||
childs = await fs.readdir(pathStr);
|
||||
} catch (error) {
|
||||
console.warn("读取报错:{}", error);
|
||||
}
|
||||
if (childs != null) {
|
||||
await readDir(childs.map(item => path.join(pathStr, item)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,4 +125,48 @@ function checkIgnore(str: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始处理
|
||||
*/
|
||||
async function work() {
|
||||
if (autoConfig == null || !autoConfig.start) {
|
||||
return;
|
||||
}
|
||||
while (needDeal.length > 0) {
|
||||
let file = needDeal.pop();
|
||||
try {
|
||||
await dealOnePath(file);
|
||||
} catch (error) {
|
||||
log.error("处理文件报错:{}", file);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一个文件路径
|
||||
* @param filePath 路径
|
||||
* @returns
|
||||
*/
|
||||
async function dealOnePath(filePath: string) {
|
||||
let exist = await fs.pathExists(filePath);
|
||||
if (!exist) {
|
||||
return;
|
||||
}
|
||||
let basePath = null;
|
||||
for (let i in autoConfig.paths) {
|
||||
if (filePath.startsWith(autoConfig.paths[i])) {
|
||||
basePath = autoConfig.paths[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (basePath == null) {
|
||||
log.warn("无法识别的文件:{}", filePath);
|
||||
return;
|
||||
}
|
||||
let relativePath = filePath.replace(basePath, "");
|
||||
let pathArrs = relativePath.split(path.sep).filter(item => item.length > 0);
|
||||
|
||||
}
|
||||
|
||||
export default AutoPlanService;
|
||||
|
23
openRenamerBackend/util/MediaUtil.ts
Normal file
23
openRenamerBackend/util/MediaUtil.ts
Normal file
@ -0,0 +1,23 @@
|
||||
const videoSet = new Set(["flv", 'avi', 'wmv', 'dat', 'vob', 'mpg', 'mpeg', 'mp4', '3gp', '3g2', 'mkv', 'rm', 'rmvb', 'mov', 'qt', 'ogg', 'ogv', 'oga', 'mod']);
|
||||
/**
|
||||
* 判断文件后缀是否为视频类型
|
||||
* @param str 文件后缀
|
||||
*/
|
||||
export function isVideo(str: string) {
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
return videoSet.has(str.toLowerCase());
|
||||
}
|
||||
|
||||
const subSet = new Set(['sub', 'sst', 'son', 'srt', 'ssa', 'ass', 'smi', 'psb', 'pjs', 'stl', 'tts', 'vsf', 'zeg']);
|
||||
/**
|
||||
* 判断文件是否为字幕文件
|
||||
* @param str 文件后缀
|
||||
*/
|
||||
export function isSub(str: string) {
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
return subSet.has(str.toLowerCase());
|
||||
}
|
@ -23,9 +23,9 @@
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="move('bottom')">
|
||||
<el-tooltip effect="dark" content="下移规则" placement="top"><el-icon>
|
||||
<bottom />
|
||||
</el-icon></el-tooltip>
|
||||
<el-tooltip effect="dark" content="下移规则" placement="top"
|
||||
><el-icon> <bottom /> </el-icon
|
||||
></el-tooltip>
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
@ -62,7 +62,7 @@ export default {
|
||||
Top,
|
||||
Bottom,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
addRuleDialogShow: false, //是否显示新增规则弹窗
|
||||
ruleTemplateShow: false, //是否显示选择规则模板弹窗
|
||||
@ -73,11 +73,11 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
//选中的规则
|
||||
checkedRules () {
|
||||
checkedRules() {
|
||||
return this.ruleList.filter((item) => item.checked);
|
||||
},
|
||||
},
|
||||
async created () {
|
||||
async created() {
|
||||
//如果外部传入了规则
|
||||
if (this.rules != undefined) {
|
||||
this.ruleList = JSON.parse(JSON.stringify(this.rules));
|
||||
@ -87,26 +87,32 @@ export default {
|
||||
await this.ruleUpdate();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
rules: function (newVal, oldVal) {
|
||||
console.log("rules变化", newVal);
|
||||
this.ruleList = JSON.parse(JSON.stringify(newVal));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
//规则更新
|
||||
ruleUpdate () {
|
||||
ruleUpdate() {
|
||||
let temp = this.ruleList.filter((item) => !item.blocked);
|
||||
this.$emit("ruleUpdate", temp);
|
||||
},
|
||||
//模板内容提交
|
||||
async templateSubmit () {
|
||||
async templateSubmit() {
|
||||
this.chosedTemplate.content = JSON.stringify(this.ruleList);
|
||||
await HttpUtil.post("/applicationRule", null, this.chosedTemplate);
|
||||
this.$message.success("操作成功");
|
||||
},
|
||||
//切换模板
|
||||
async templateUpdate (newVal) {
|
||||
async templateUpdate(newVal) {
|
||||
this.ruleList = JSON.parse(newVal.content);
|
||||
this.ruleUpdate();
|
||||
this.ruleTemplateShow = false;
|
||||
},
|
||||
//新增规则
|
||||
async ruleAdd (data) {
|
||||
async ruleAdd(data) {
|
||||
if (this.editRule != null) {
|
||||
let index = this.ruleList.indexOf(this.editRule);
|
||||
this.ruleList.splice(index, 1, data);
|
||||
@ -119,7 +125,7 @@ export default {
|
||||
this.addRuleDialogShow = false;
|
||||
},
|
||||
//禁用/启用
|
||||
async block () {
|
||||
async block() {
|
||||
this.ruleList
|
||||
.filter((item) => item.checked)
|
||||
.forEach((item) => {
|
||||
@ -129,17 +135,17 @@ export default {
|
||||
await this.ruleUpdate();
|
||||
},
|
||||
//删除规则
|
||||
async deleteRule () {
|
||||
async deleteRule() {
|
||||
this.ruleList = this.ruleList.filter((item) => !item.checked);
|
||||
this.ruleUpdate();
|
||||
},
|
||||
//编辑规则
|
||||
editClick (rule) {
|
||||
editClick(rule) {
|
||||
this.editRule = rule && rule.data ? rule : this.checkedRules[0];
|
||||
this.addRuleDialogShow = true;
|
||||
},
|
||||
//移动规则
|
||||
async move (type) {
|
||||
async move(type) {
|
||||
let index = this.ruleList.indexOf(this.checkedRules[0]);
|
||||
let newIndex;
|
||||
if (type == "top") {
|
||||
@ -160,7 +166,7 @@ export default {
|
||||
await this.ruleUpdate();
|
||||
},
|
||||
//规则弹窗关闭
|
||||
ruleDialogClose () {
|
||||
ruleDialogClose() {
|
||||
this.editRule = null;
|
||||
this.addRuleDialogShow = false;
|
||||
},
|
||||
|
@ -1,28 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form ref="configRuleRef" label-width="100px" :model="body" :rules="bodyRule">
|
||||
<input type="text" class="form-control" style="display:none" />
|
||||
<input type="text" class="form-control" style="display: none" />
|
||||
<el-form-item label="剧集目录" prop="paths">
|
||||
<div style="text-align:left">
|
||||
<div style="text-align: left">
|
||||
<template v-for="(item, index) in body.paths" :key="item">
|
||||
<el-tag closable @close="closePath(index, 'folder')">{{ item }}</el-tag> <br />
|
||||
</template>
|
||||
<div style="display:flex;align-items:center">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-button type="primary" link @click="showFolderDialog = true">+新增目录</el-button>
|
||||
<tips message="添加剧集的上级目录,此目录下的每一个文件夹都将被认为是一部剧" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="忽略文件">
|
||||
<div style="text-align:left;display:flex;align-items:center">
|
||||
<div style="text-align: left; display: flex; align-items: center">
|
||||
<template v-for="(item, index) in body.ignorePaths" :key="item">
|
||||
<el-tag closable @close="closePath(index, 'ignore')">{{ item }}</el-tag> <br />
|
||||
</template>
|
||||
<el-input v-if="ignoreInputVisible" v-model="ignoreInput" class="ml-1 w-20" size="small"
|
||||
@keyup.enter="addIngoreFile" @blur="addIngoreFile" />
|
||||
<el-button v-else class="button-new-tag ml-1" size="small" @click="ignoreInputVisible = true">
|
||||
+ 忽略
|
||||
</el-button>
|
||||
<el-input
|
||||
v-if="ignoreInputVisible"
|
||||
v-model="ignoreInput"
|
||||
class="ml-1 w-20"
|
||||
size="small"
|
||||
@keyup.enter="addIngoreFile"
|
||||
@blur="addIngoreFile"
|
||||
/>
|
||||
<el-button v-else class="button-new-tag ml-1" size="small" @click="ignoreInputVisible = true"> + 忽略 </el-button>
|
||||
<tips message="名字匹配的文件/文件夹将会忽略处理,支持js正则表达式" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
@ -56,8 +60,8 @@
|
||||
<script setup>
|
||||
import FileChose from "@/components/FileChose.vue";
|
||||
import RuleBlock from "@/components/rules/RuleBlock.vue";
|
||||
import Tips from '@/components/Tips.vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Tips from "@/components/Tips.vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import http from "@/utils/HttpUtil";
|
||||
|
||||
@ -66,30 +70,29 @@ let body = ref({
|
||||
paths: [],
|
||||
version: 1,
|
||||
ignoreSeason0: true,
|
||||
ignorePaths: ['.*\.jpg', 'tvshow.nfo', 'season.nfo', 'metadata'],
|
||||
ignorePaths: [".*\.jpg", "tvshow.nfo", "season.nfo", "metadata"],
|
||||
deleteSmallVideo: true,
|
||||
rules: [],
|
||||
ignoreExist: false,
|
||||
start: false
|
||||
start: false,
|
||||
});
|
||||
let bodyRule = reactive({
|
||||
paths: { type: 'array', required: true, message: '目录不能为空', trigger: 'change' },
|
||||
rules: { type: 'array', required: true, message: '目录不能为空', trigger: 'change' },
|
||||
})
|
||||
paths: { type: "array", required: true, message: "目录不能为空", trigger: "change" },
|
||||
rules: { type: "array", required: true, message: "目录不能为空", trigger: "change" },
|
||||
});
|
||||
const configRuleRef = ref();
|
||||
let showFolderDialog = ref(false);
|
||||
let ignoreInputVisible = ref(false);
|
||||
let ignoreInput = ref("");
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
let res = await http.post("/config/multCode", null, ['autoConfig', 'firstUse']);
|
||||
let res = await http.post("/config/multCode", null, ["autoConfig", "firstUse"]);
|
||||
if (res.autoConfig == undefined && res.firstUse == undefined) {
|
||||
await http.post("/config/insertOrUpdate", null, { code: "firstUse", val: "1" });
|
||||
await ElMessageBox.alert("似乎是首次使用自动化,是否需要查看使用文档?", "提示", {
|
||||
confirmButtonText: "是",
|
||||
cancelButtonText: "否",
|
||||
showCancelButton: true
|
||||
showCancelButton: true,
|
||||
});
|
||||
alert("跳转到自动化使用文档");
|
||||
return;
|
||||
@ -100,18 +103,18 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
//新增文件夹
|
||||
async function folderChose (data) {
|
||||
async function folderChose(data) {
|
||||
if (body.value.paths.indexOf(data) > -1) {
|
||||
ElMessage({ type: 'warning', message: "路径已存在" });
|
||||
ElMessage({ type: "warning", message: "路径已存在" });
|
||||
return;
|
||||
}
|
||||
body.value.paths.push(data);
|
||||
showFolderDialog.value = false;
|
||||
}
|
||||
//新增忽略文件
|
||||
async function addIngoreFile () {
|
||||
async function addIngoreFile() {
|
||||
if (body.value.ignorePaths.indexOf(ignoreInput.value) > -1) {
|
||||
ElMessage({ type: 'warning', message: "名称已存在" });
|
||||
ElMessage({ type: "warning", message: "名称已存在" });
|
||||
return;
|
||||
}
|
||||
if (ignoreInput.value.length > 0) {
|
||||
@ -123,23 +126,23 @@ async function addIngoreFile () {
|
||||
}
|
||||
|
||||
//删除路径
|
||||
async function closePath (index, type) {
|
||||
(type === 'folder' ? body.value.paths : body.value.ignorePaths).splice(index, 1);
|
||||
async function closePath(index, type) {
|
||||
(type === "folder" ? body.value.paths : body.value.ignorePaths).splice(index, 1);
|
||||
}
|
||||
//更新规则
|
||||
function ruleUpdate (rules) {
|
||||
function ruleUpdate(rules) {
|
||||
body.value.rules = rules;
|
||||
}
|
||||
//提交
|
||||
async function submit () {
|
||||
async function submit() {
|
||||
console.dir(configRuleRef.value);
|
||||
configRuleRef.value.validate(async valid => {
|
||||
configRuleRef.value.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
await http.post("/config/insertOrUpdate", null, { code: "autoConfig", val: JSON.stringify(body.value), description: "自动化配置" });
|
||||
ElMessage({ type: 'success', message: "保存成功" });
|
||||
})
|
||||
await http.post("/autoPlan/save", null, body.value);
|
||||
ElMessage({ type: "success", message: "保存成功" });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user