From b79615f620066ba6ade2565b2a9c62e8715d9b1d Mon Sep 17 00:00:00 2001 From: thsrite Date: Thu, 27 Jun 2024 10:24:40 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E5=AA=92=E4=BD=93=E5=BA=93=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=AA=92=E4=BD=93=E6=A3=80=E6=B5=8Bv1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- icons/libraryduplicate.png | Bin 0 -> 5339 bytes package.json | 12 + plugins/libraryduplicatecheck/__init__.py | 421 ++++++++++++++++++++++ 4 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 icons/libraryduplicate.png create mode 100644 plugins/libraryduplicatecheck/__init__.py diff --git a/README.md b/README.md index 3d0180d..e244777 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,5 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - 云盘助手(docs%2FCloudAssistant.md) v2.0.3 - CloudDrive2助手 v1.2 - 软连接重定向 v1.0 -- 云盘同步删除 v1.2 \ No newline at end of file +- 云盘同步删除 v1.2 +- 媒体库重复媒体检测 v1.0 \ No newline at end of file diff --git a/icons/libraryduplicate.png b/icons/libraryduplicate.png new file mode 100644 index 0000000000000000000000000000000000000000..ab39738525dda266b749c2740d25a041a1c92c2f GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index b55d793..0458dbf 100644 --- a/package.json +++ b/package.json @@ -582,5 +582,17 @@ "v1.1": "增加测试模式按钮(不删除文件)", "v1.0": "媒体库删除软连接文件后,同步删除云盘文件" } + }, + "LibraryDuplicateCheck": { + "name": "媒体库重复媒体检测", + "description": "媒体库重复媒体检查,可选择保留规则保留其一。", + "labels": "媒体库", + "version": "1.0", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/libraryduplicate.png", + "author": "thsrite", + "level": 2, + "history": { + "v1.0": "媒体库重复媒体检查,可选择保留规则保留其一" + } } } diff --git a/plugins/libraryduplicatecheck/__init__.py b/plugins/libraryduplicatecheck/__init__.py new file mode 100644 index 0000000..11abf9b --- /dev/null +++ b/plugins/libraryduplicatecheck/__init__.py @@ -0,0 +1,421 @@ +from datetime import datetime, timedelta +import os +from collections import defaultdict +from pathlib import Path +import pytz + +from app.core.config import settings +from app.plugins import _PluginBase +from typing import Any, List, Dict, Tuple, Optional +from app.log import logger +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from app.schemas.types import EventType +from app.utils.system import SystemUtils + + +class LibraryDuplicateCheck(_PluginBase): + # 插件名称 + plugin_name = "媒体库重复媒体检测。" + # 插件描述 + plugin_desc = "媒体库重复媒体检查,可选择保留规则保留其一。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/libraryduplicate.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "libraryduplicatecheck_" + # 加载顺序 + plugin_order = 9 + # 可使用的用户级别 + auth_level = 2 + + # 私有属性 + _enabled = False + # 任务执行间隔 + _paths = {} + _notify = False + _delete_softlink = False + _cron = None + _onlyonce = False + _retain_type = None + _rmt_mediaext = ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v" + + _scheduler: Optional[BackgroundScheduler] = None + + def init_plugin(self, config: dict = None): + if config: + self._enabled = config.get("enabled") + self._notify = config.get("notify") + self._cron = config.get("cron") + self._delete_softlink = config.get("delete_softlink") + self._onlyonce = config.get("onlyonce") + self._retain_type = config.get("retain_type") + self._paths = config.get("paths") + self._rmt_mediaext = config.get( + "rmt_mediaext") or ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v" + + if self._enabled or self._onlyonce: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + # 立即运行一次 + if self._onlyonce: + logger.info(f"媒体库重复媒体检测服务启动,立即运行一次") + self._scheduler.add_job(self.check_duplicate, 'date', + run_date=datetime.now( + tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="媒体库重复媒体检测") + # 关闭一次性开关 + self._onlyonce = False + + # 保存配置 + self.__update_config() + + # 周期运行 + if self._cron: + try: + self._scheduler.add_job(func=self.check_duplicate, + trigger=CronTrigger.from_crontab(self._cron), + name="媒体库重复媒体检测") + except Exception as err: + logger.error(f"定时任务配置错误:{err}") + # 推送实时消息 + self.systemmessage.put(f"执行周期配置错误:{err}") + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def check_duplicate(self): + """ + 检查媒体库重复媒体 + """ + if not self._paths: + logger.warning("媒体库重复媒体检测服务未配置路径") + return + + for path in self._paths.split("\n"): + logger.info(f"开始检查路径:{path}") + self.__find_duplicate_videos(path) + + def __find_duplicate_videos(self, directory): + """ + 检查目录下视频文件是否有重复 + """ + # Dictionary to hold the list of files for each video name + video_files = defaultdict(list) + + # Traverse the directory and subdirectories + for root, _, files in os.walk(directory): + for file in files: + # Check the file extension + if Path(file).suffix.lower() in [ext.strip() for ext in + self._rmt_mediaext.split(",")]: + video_name = Path(file).stem.split('-')[0].rstrip() + logger.info(f'Scan file -> {file} -> {video_name}') + video_files[video_name].append(os.path.join(root, file)) + + logger.info() + logger.info("================== RESULT ==================") + # Find and handle duplicate video files + for name, paths in video_files.items(): + if len(paths) > 1: + logger.info(f"Duplicate video files for '{name}':") + for path in paths: + logger.info(f" {path} 文件大小:{os.path.getsize(path)},创建时间:{os.path.getmtime(path)}") + + if str(self._retain_type) != "仅检查": + # Decide which file to keep based on criteria (e.g., file size or creation date) + keep_path = self.__choose_file_to_keep(paths) + logger.info(f"文件保留规则:{str(self._retain_type)} Keeping: {keep_path}") + # Delete the other duplicate files (if needed) + for path in paths: + if path != keep_path: + cloud_file = os.readlink(path) + # Path(path).unlink() + logger.info(f"Deleted Local file: {path}") + self.__rmtree(Path(path), "监控") + + # 同步删除软连接源目录 + if cloud_file and self._delete_softlink: + logger.info(f"开始删除云盘文件 {cloud_file}") + if Path(cloud_file).exists(): + cloud_file_path = Path(cloud_file) + # 删除文件、nfo、jpg等同名文件 + pattern = cloud_file_path.stem.replace('[', '?').replace(']', '?') + logger.info(f"开始筛选 {cloud_file_path.parent} 下同名文件 {pattern}") + files = cloud_file_path.parent.glob(f"{pattern}.*") + for file in files: + # Path(file).unlink() + logger.info(f"云盘文件 {file} 已删除") + self.__rmtree(cloud_file_path, "云盘") + + else: + logger.info(f"'{name}' No Duplicate video files.") + + def __rmtree(self, path: Path, file_type: str): + """ + 删除目录及其子目录 + """ + # 判断当前媒体父路径下是否有媒体文件,如有则无需遍历父级 + if not SystemUtils.exits_files(path.parent, [ext.strip() for ext in + self._rmt_mediaext.split(",")]): + # 判断父目录是否为空, 为空则删除 + for parent_path in path.parents: + if str(parent_path.parent) != str(path.root): + # 父目录非根目录,才删除父目录 + if not SystemUtils.exits_files(parent_path, [ext.strip() for ext in + self._rmt_mediaext.split(",")]): + # 当前路径下没有媒体文件则删除 + # shutil.rmtree(parent_path) + logger.warn(f"{file_type}目录 {parent_path} 已删除") + + @staticmethod + def __choose_file_to_keep(paths): + # Example: Choose based on file size (keeping the smallest) + smallest_size = float('inf') + smallest_path = None + for path in paths: + file_size = os.path.getmtime(path) + if file_size < smallest_size: + smallest_size = file_size + smallest_path = path + return smallest_path + + def __update_config(self): + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "cron": self._cron, + "delete_softlink": self._delete_softlink, + "notify": self._notify, + "paths": self._paths, + "retain_type": self._retain_type, + "rmt_mediaext": self._rmt_mediaext + }) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + return [{ + "cmd": "/libraryduplicatecheck", + "event": EventType.PluginAction, + "desc": "媒体库重复媒体检测", + "category": "", + "data": { + "action": "libraryduplicatecheck" + } + }] + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'notify', + 'label': '开启通知', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'delete_softlink', + 'label': '删除软连接源文件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 9 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'multiple': False, + 'chips': True, + 'model': 'retain_type', + 'label': '质量', + 'items': [ + {'title': '仅检查', 'value': '仅检查'}, + {'title': '保留体积最小', 'value': '保留体积最小'}, + {'title': '保留体积最大', 'value': '保留体积最大'}, + {'title': '保留创建最早', 'value': '保留创建最早'}, + {'title': '保留创建最晚', 'value': '保留创建最晚'}, + ] + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'paths', + 'label': '检查路径', + 'rows': 2, + 'placeholder': "一行一个" + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'rmt_mediaext', + 'label': '视频格式', + 'rows': 2, + 'placeholder': ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v" + } + } + ] + } + ] + }, + ] + } + ], { + "enabled": False, + "onlyonce": False, + "delete_softlink": False, + "cron": "5 1 * * *", + "paths": "", + "notify": False, + "retain_type": "仅检查", + "rmt_mediaext": ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v" + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error("退出插件失败:%s" % str(e))