Merge branch 'dev' of ssh://gitea.fleyx.com:222/fanxb/open-renamer into dev

# Conflicts:
#	openRenamerBackend/api/AutoPlanApi.ts
This commit is contained in:
fanxb 2023-03-07 00:09:15 +08:00
commit d52398f14c
6 changed files with 277 additions and 149 deletions

View 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;

View File

@ -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);
}

View File

@ -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;

View 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());
}

View File

@ -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;
},

View File

@ -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>