diff --git a/package.json b/package.json index a1cc3ff..d15604e 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "LibraryScraper": { "name": "媒体库刮削", "description": "定时对媒体库进行刮削,补齐缺失元数据和图片。", - "version": "1.0", + "version": "1.1", "icon": "scraper.png", "color": "#FF7D00", "author": "jxxghp", @@ -92,7 +92,7 @@ "MediaSyncDel": { "name": "媒体文件同步删除", "description": "同步删除历史记录、源文件和下载任务。", - "version": "1.1", + "version": "1.2", "icon": "mediasyncdel.png", "color": "#ff1a1a", "author": "thsrite", @@ -137,7 +137,7 @@ "MediaServerMsg": { "name": "媒体库服务器通知", "description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。", - "version": "1.0", + "version": "1.1", "icon": "mediaplay.png", "color": "#42A3DB", "author": "jxxghp", @@ -263,7 +263,7 @@ "InvitesSignin": { "name": "药丸签到", "description": "药丸论坛签到。", - "version": "1.0", + "version": "1.1", "icon": "invites.png", "color": "#FFFFFF", "author": "thsrite", @@ -290,7 +290,7 @@ "CloudDiskDel": { "name": "云盘文件删除", "description": "媒体库删除strm文件后同步删除云盘资源。", - "version": "1.0", + "version": "1.1", "icon": "clouddisk.png", "color": "#4285F5", "author": "thsrite", diff --git a/plugins/clouddiskdel/__init__.py b/plugins/clouddiskdel/__init__.py index 4af40e0..d6d1edc 100644 --- a/plugins/clouddiskdel/__init__.py +++ b/plugins/clouddiskdel/__init__.py @@ -1,4 +1,3 @@ -import os import shutil import time from pathlib import Path @@ -23,7 +22,7 @@ class CloudDiskDel(_PluginBase): # 主题色 plugin_color = "#4285F5" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -40,15 +39,29 @@ class CloudDiskDel(_PluginBase): # 任务执行间隔 _paths = {} _notify = False + _del_history = False + + _video_formats = ('.mp4', '.avi', '.rmvb', '.wmv', '.mov', '.mkv', '.flv', '.ts', '.webm', '.iso', '.mpg') def init_plugin(self, config: dict = None): if config: self._enabled = config.get("enabled") self._notify = config.get("notify") + self._del_history = config.get("del_history") for path in str(config.get("path")).split("\n"): paths = path.split(":") self._paths[paths[0]] = paths[1] + # 清理插件历史 + if self._del_history: + self.del_data(key="history") + self.update_config({ + "enabled": self._enabled, + "notify": self._notify, + "path": config.get("path"), + "del_history": False + }) + @eventmanager.register(EventType.NetworkDiskDel) def clouddisk_del(self, event: Event): if not self._enabled: @@ -80,9 +93,19 @@ class CloudDiskDel(_PluginBase): pattern = path.stem.replace('[', '?').replace(']', '?') logger.info(f"开始筛选同名文件 {pattern}") files = path.parent.glob(f"{pattern}.*") + + remove_flag = False for file in files: Path(file).unlink() logger.info(f"云盘文件 {file} 已删除") + remove_flag = True + + if not remove_flag: + for ext in self._video_formats: + file = path.stem + ext + if Path(file).exists(): + Path(file).unlink() + logger.info(f"云盘文件 {file} 已删除") else: # 非根目录,才删除目录 shutil.rmtree(path) @@ -151,8 +174,8 @@ class CloudDiskDel(_PluginBase): "type": media_type.value, "title": media_name, "path": media_path, - "season": season_num, - "episode": episode_num, + "season": season_num if season_num and str(season_num).isdigit() else None, + "episode": episode_num if episode_num and str(episode_num).isdigit() else None, "image": poster_image, "del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) }) @@ -185,7 +208,7 @@ class CloudDiskDel(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -201,7 +224,7 @@ class CloudDiskDel(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -212,6 +235,22 @@ class CloudDiskDel(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'del_history', + 'label': '删除历史', + } + } + ] } ] }, @@ -289,7 +328,8 @@ class CloudDiskDel(_PluginBase): ], { "enabled": False, "path": "", - "notify": False + "notify": False, + "del_history": False } def get_page(self) -> List[dict]: diff --git a/plugins/invitessignin/__init__.py b/plugins/invitessignin/__init__.py index 8e2c516..cbcdaa0 100644 --- a/plugins/invitessignin/__init__.py +++ b/plugins/invitessignin/__init__.py @@ -24,7 +24,7 @@ class InvitesSignin(_PluginBase): # 主题色 plugin_color = "#FFFFFF" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -157,6 +157,17 @@ class InvitesSignin(_PluginBase): text=f"累计签到 {totalContinuousCheckIn} \n" f"剩余药丸 {money}") + # 读取历史记录 + history = self.get_data('history') or [] + + history.append({ + "date": datetime.today().strftime('%Y-%m-%d %H:%M:%S'), + "totalContinuousCheckIn": totalContinuousCheckIn, + "money": money + }) + # 保存签到历史 + self.save_data(key="history", value=history) + def get_state(self) -> bool: return self._enabled @@ -172,131 +183,232 @@ class InvitesSignin(_PluginBase): 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 """ return [ - { - 'component': 'VForm', - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enabled', - 'label': '启用插件', - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'notify', - 'label': '开启通知', - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'onlyonce', - 'label': '立即运行一次', - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'cron', - 'label': '签到周期' - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'cookie', - 'label': '药丸cookie' - } - } - ] - } - ] - }, - { - 'component': 'VRow', + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, 'content': [ { - 'component': 'VCol', + 'component': 'VSwitch', 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '整点定时签到失败?不妨换个时间试试' - } - } - ] + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'notify', + 'label': '开启通知', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } } ] } - ] - } - ], { - "enabled": False, - "onlyonce": False, - "notify": False, - "cookie": "", - "cron": "0 9 * * *" - } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '签到周期' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cookie', + 'label': '药丸cookie' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '整点定时签到失败?不妨换个时间试试' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "onlyonce": False, + "notify": False, + "cookie": "", + "cron": "0 9 * * *" + } def get_page(self) -> List[dict]: + # 查询同步详情 + historys = self.get_data('history') + if not historys: + return [ + { + 'component': 'div', + 'text': '暂无数据', + 'props': { + 'class': 'text-center', + } + } + ] + + if not isinstance(historys, list): + historys = [historys] + + # 按照签到时间倒序 + historys = sorted(historys, key=lambda x: x.get("date") or 0, reverse=True) + + # 签到消息 + sign_msgs = [ + { + 'component': 'tr', + 'props': { + 'class': 'text-sm' + }, + 'content': [ + { + 'component': 'td', + 'props': { + 'class': 'whitespace-nowrap break-keep text-high-emphasis' + }, + 'text': history.get("date") + }, + { + 'component': 'td', + 'text': history.get("totalContinuousCheckIn") + }, + { + 'component': 'td', + 'text': history.get("money") + } + ] + } for history in historys + ] + + # 拼装页面 + return [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTable', + 'props': { + 'hover': True + }, + 'content': [ + { + 'component': 'thead', + 'content': [ + { + 'component': 'th', + 'props': { + 'class': 'text-start ps-4' + }, + 'text': '时间' + }, + { + 'component': 'th', + 'props': { + 'class': 'text-start ps-4' + }, + 'text': '连续签到次数' + }, + { + 'component': 'th', + 'props': { + 'class': 'text-start ps-4' + }, + 'text': '剩余药丸' + }, + ] + }, + { + 'component': 'tbody', + 'content': sign_msgs + } + ] + } + ] + } + ] + } + ] + pass def stop_service(self): diff --git a/plugins/libraryscraper/__init__.py b/plugins/libraryscraper/__init__.py index 6a638bc..4034236 100644 --- a/plugins/libraryscraper/__init__.py +++ b/plugins/libraryscraper/__init__.py @@ -19,7 +19,6 @@ from app.utils.system import SystemUtils class LibraryScraper(_PluginBase): - # 插件名称 plugin_name = "媒体库刮削" # 插件描述 @@ -29,7 +28,7 @@ class LibraryScraper(_PluginBase): # 主题色 plugin_color = "#FF7D00" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -54,7 +53,7 @@ class LibraryScraper(_PluginBase): _exclude_paths = "" # 退出事件 _event = Event() - + def init_plugin(self, config: dict = None): # 读取配置 if config: @@ -90,7 +89,7 @@ class LibraryScraper(_PluginBase): logger.info(f"媒体库刮削服务,立即运行一次") self._scheduler.add_job(func=self.__libraryscraper, trigger='date', run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), - name="Cloudflare优选") + name="媒体库刮削") # 关闭一次性开关 self._onlyonce = False self.update_config({ @@ -245,6 +244,49 @@ class LibraryScraper(_PluginBase): ] } ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '刮削路径要配置到二级分类路径。(如果配置了LIBRARY_CATEGORY=true)' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '刮削路径后拼接#电视剧/电影,强制指定该媒体路径媒体类型。' + '不加默认根据文件名自动识别媒体类型。' + } + } + ] + } + ] } ] } @@ -272,11 +314,19 @@ class LibraryScraper(_PluginBase): for path in paths: if not path: continue + # 强制指定该路径媒体类型 + mtype = None + if str(path).count("#") == 1: + mtype = next( + (mediaType for mediaType in MediaType.__members__.values() if + mediaType.value == str(str(path).split("#")[1])), + None) + path = str(path).split("#")[0] scraper_path = Path(path) if not scraper_path.exists(): logger.warning(f"媒体库刮削路径不存在:{path}") continue - logger.info(f"开始刮削媒体库:{path} ...") + logger.info(f"开始刮削媒体库:{path} {mtype} ...") # 遍历一层文件夹 for sub_path in scraper_path.iterdir(): if self._event.is_set(): @@ -302,11 +352,11 @@ class LibraryScraper(_PluginBase): logger.warn(f"{sub_path} 可能不是媒体目录,请检查刮削目录配置,跳过 ...") continue logger.info(f"开始刮削目录:{sub_path} ...") - self.__scrape_dir(path=sub_path, dir_meta=dir_meta) + self.__scrape_dir(path=sub_path, dir_meta=dir_meta, mtype=mtype) logger.info(f"目录 {sub_path} 刮削完成") logger.info(f"媒体库 {path} 刮削完成") - def __scrape_dir(self, path: Path, dir_meta: MetaBase): + def __scrape_dir(self, path: Path, dir_meta: MetaBase, mtype: MediaType = None): """ 削刮一个目录,该目录必须是媒体文件目录 """ @@ -325,6 +375,10 @@ class LibraryScraper(_PluginBase): meta_info = MetaInfo(file.stem) # 合并 meta_info.merge(dir_meta) + # 强制指定类型 + if mtype: + meta_info.type = mtype + # 是否刮削 scrap_metadata = settings.SCRAP_METADATA diff --git a/plugins/mediaservermsg/__init__.py b/plugins/mediaservermsg/__init__.py index d0cbf73..e4c394a 100644 --- a/plugins/mediaservermsg/__init__.py +++ b/plugins/mediaservermsg/__init__.py @@ -19,7 +19,7 @@ class MediaServerMsg(_PluginBase): # 主题色 plugin_color = "#42A3DB" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -80,6 +80,8 @@ class MediaServerMsg(_PluginBase): {"title": "停止播放", "value": "playback.stop|media.stop|PlaybackStop"}, {"title": "用户标记", "value": "item.rate"}, {"title": "测试", "value": "system.webhooktest"}, + {"title": "登录成功", "value": "user.authenticated"}, + {"title": "登录失败", "value": "user.authenticationfailed"}, ] return [ { diff --git a/plugins/mediasyncdel/__init__.py b/plugins/mediasyncdel/__init__.py index 12f61e0..8355169 100644 --- a/plugins/mediasyncdel/__init__.py +++ b/plugins/mediasyncdel/__init__.py @@ -33,7 +33,7 @@ class MediaSyncDel(_PluginBase): # 主题色 plugin_color = "#ff1a1a" # 插件版本 - plugin_version = "1.1" + plugin_version = "1.2" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -53,6 +53,7 @@ class MediaSyncDel(_PluginBase): _cron: str = "" _notify = False _del_source = False + _del_history = False _exclude_path = None _library_path = None _transferchain = None @@ -79,22 +80,39 @@ class MediaSyncDel(_PluginBase): self._cron = config.get("cron") self._notify = config.get("notify") self._del_source = config.get("del_source") + self._del_history = config.get("del_history") self._exclude_path = config.get("exclude_path") self._library_path = config.get("library_path") + # 清理插件历史 + if self._del_history: + self.del_data(key="history") + self.update_config({ + "enabled": self._enabled, + "sync_type": self._sync_type, + "cron": self._cron, + "notify": self._notify, + "del_source": self._del_source, + "del_history": False, + "exclude_path": self._exclude_path, + "library_path": self._library_path + }) + if self._enabled and str(self._sync_type) == "log": self._scheduler = BackgroundScheduler(timezone=settings.TZ) + # 媒体库同步删除日志方式 if self._cron: try: self._scheduler.add_job(func=self.sync_del_by_log, trigger=CronTrigger.from_crontab(self._cron), - name="媒体库同步删除") + name="媒体库同步删除日志方式") except Exception as err: logger.error(f"定时任务配置错误:{str(err)}") # 推送实时消息 self.systemmessage.put(f"执行周期配置错误:{str(err)}") else: - self._scheduler.add_job(self.sync_del_by_log, "interval", minutes=30, name="媒体库同步删除") + self._scheduler.add_job(self.sync_del_by_log, "interval", minutes=30, + name="媒体库同步删除日志方式") # 启动任务 if self._scheduler.get_jobs(): @@ -127,7 +145,7 @@ class MediaSyncDel(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -143,7 +161,7 @@ class MediaSyncDel(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -159,7 +177,7 @@ class MediaSyncDel(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -170,6 +188,22 @@ class MediaSyncDel(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'del_history', + 'label': '删除历史', + } + } + ] } ] }, @@ -293,11 +327,11 @@ class MediaSyncDel(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '关于路径映射(转移后文件):' - 'emby:/data/series/A.mp4,' - 'moviepilot:/mnt/link/series/A.mp4。' + 'text': '关于路径映射(转移后文件路径):' + 'emby:/data/A.mp4,' + 'moviepilot:/mnt/link/A.mp4。' '路径映射填/data:/mnt/link。' - '不正确配置会导致查询不到转移记录!' + '不正确配置会导致查询不到转移记录!(路径一样可不填)' } } ] @@ -324,6 +358,51 @@ class MediaSyncDel(_PluginBase): ] } ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': 'Scripter X配置文档:' + 'https://github.com/thsrite/' + 'MediaSyncDel/blob/main/MoviePilot/MoviePilot.md' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '路径映射配置文档:' + 'https://github.com/thsrite/MediaSyncDel/blob/main/path.md' + } + } + ] + } + ] } ] } @@ -331,6 +410,7 @@ class MediaSyncDel(_PluginBase): "enabled": False, "notify": True, "del_source": False, + "del_history": False, "library_path": "", "sync_type": "webhook", "cron": "*/30 * * * *", @@ -518,6 +598,25 @@ class MediaSyncDel(_PluginBase): # 集数 episode_num = event_data.episode_id + """ + 执行删除逻辑 + """ + if self._exclude_path and media_path and any( + os.path.abspath(media_path).startswith(os.path.abspath(path)) for path in + self._exclude_path.split(",")): + logger.info(f"媒体路径 {media_path} 已被排除,暂不处理") + # 发送消息通知网盘删除插件删除网盘资源 + self.eventmanager.send_event(EventType.NetworkDiskDel, + { + "media_path": media_path, + "media_name": media_name, + "tmdb_id": tmdb_id, + "media_type": media_type, + "season_num": season_num, + "episode_num": episode_num, + }) + return + # 兼容emby webhook season删除没有发送tmdbid if not tmdb_id and str(media_type) != 'Season': logger.error(f"{media_name} 同步删除失败,未获取到TMDB ID,请检查媒体库媒体是否刮削") @@ -578,19 +677,6 @@ class MediaSyncDel(_PluginBase): # 集数 episode_num = event_data.episode_id - if not tmdb_id or not str(tmdb_id).isdigit(): - logger.error(f"{media_name} 同步删除失败,未获取到TMDB ID,请检查媒体库媒体是否刮削") - return - - self.__sync_del(media_type=media_type, - media_name=media_name, - media_path=media_path, - tmdb_id=tmdb_id, - season_num=season_num, - episode_num=episode_num) - - def __sync_del(self, media_type: str, media_name: str, media_path: str, - tmdb_id: int, season_num: str, episode_num: str): """ 执行删除逻辑 """ @@ -610,6 +696,19 @@ class MediaSyncDel(_PluginBase): }) return + if not tmdb_id or not str(tmdb_id).isdigit(): + logger.error(f"{media_name} 同步删除失败,未获取到TMDB ID,请检查媒体库媒体是否刮削") + return + + self.__sync_del(media_type=media_type, + media_name=media_name, + media_path=media_path, + tmdb_id=tmdb_id, + season_num=season_num, + episode_num=episode_num) + + def __sync_del(self, media_type: str, media_name: str, media_path: str, + tmdb_id: int, season_num: str, episode_num: str): if not media_type: logger.error(f"{media_name} 同步删除失败,未获取到媒体类型,请检查媒体是否刮削") return @@ -656,6 +755,7 @@ class MediaSyncDel(_PluginBase): try: # 2、判断种子是否被删除完 delete_flag, success_flag, handle_torrent_hashs = self.handle_torrent( + type=transferhis.type, src=transferhis.src, torrent_hash=transferhis.download_hash) if not success_flag: @@ -807,7 +907,7 @@ class MediaSyncDel(_PluginBase): """ # 读取历史记录 history = self.get_data('history') or [] - last_time = self.get_data("last_time") + last_time = self.get_data("last_time") or None del_medias = [] # 媒体服务器类型,多个以,分隔 @@ -832,7 +932,7 @@ class MediaSyncDel(_PluginBase): for del_media in del_medias: # 删除时间 del_time = del_media.get("time") - last_del_time = del_time + last_del_time = del_time or datetime.datetime.now() # 媒体类型 Movie|Series|Season|Episode media_type = del_media.get("type") # 媒体名称 蜀山战纪 @@ -851,7 +951,7 @@ class MediaSyncDel(_PluginBase): os.path.abspath(media_path).startswith(os.path.abspath(path)) for path in self._exclude_path.split(",")): logger.info(f"媒体路径 {media_path} 已被排除,暂不处理") - self.save_data("last_time", last_del_time or datetime.datetime.now()) + self.save_data("last_time", last_del_time) return # 处理路径映射 (处理同一媒体多分辨率的情况) @@ -894,14 +994,14 @@ class MediaSyncDel(_PluginBase): episode=media_episode, dest=media_path) else: - self.save_data("last_time", last_del_time or datetime.datetime.now()) + self.save_data("last_time", last_del_time) continue logger.info(f"正在同步删除 {msg}") if not transfer_history: logger.info(f"未获取到 {msg} 转移记录,请检查路径映射是否配置错误,请检查tmdbid获取是否正确") - self.save_data("last_time", last_del_time or datetime.datetime.now()) + self.save_data("last_time", last_del_time) continue logger.info(f"获取到删除历史记录数量 {len(transfer_history)}") @@ -916,7 +1016,7 @@ class MediaSyncDel(_PluginBase): if title not in media_name: logger.warn( f"当前转移记录 {transferhis.id} {title} {transferhis.tmdbid} 与删除媒体{media_name}不符,防误删,暂不自动删除") - self.save_data("last_time", last_del_time or datetime.datetime.now()) + self.save_data("last_time", last_del_time) continue image = transferhis.image or image # 0、删除转移记录 @@ -931,6 +1031,7 @@ class MediaSyncDel(_PluginBase): try: # 2、判断种子是否被删除完 delete_flag, success_flag, handle_torrent_hashs = self.handle_torrent( + type=transferhis.type, src=transferhis.src, torrent_hash=transferhis.download_hash) if not success_flag: @@ -981,9 +1082,9 @@ class MediaSyncDel(_PluginBase): # 保存历史 self.save_data("history", history) - self.save_data("last_time", last_del_time or datetime.datetime.now()) + self.save_data("last_time", last_del_time) - def handle_torrent(self, src: str, torrent_hash: str): + def handle_torrent(self, type: str, src: str, torrent_hash: str): """ 判断种子是否局部删除 局部删除则暂停种子 @@ -1085,17 +1186,80 @@ class MediaSyncDel(_PluginBase): handle_torrent_hashs.append(download_id) # 处理辅种 - handle_cnt = self.__del_seed(download=download, - download_id=download_id, - action_flag="del" if delete_flag else 'stop', - handle_torrent_hashs=handle_torrent_hashs) - - return delete_flag, True, handle_cnt + handle_torrent_hashs = self.__del_seed(download=download, + download_id=download_id, + delete_flag=delete_flag, + handle_torrent_hashs=handle_torrent_hashs) + # 处理合集 + if str(type) == "电视剧": + handle_torrent_hashs = self.__del_collection(src=src, + delete_flag=delete_flag, + torrent_hash=torrent_hash, + download_files=download_files, + handle_torrent_hashs=handle_torrent_hashs) + return delete_flag, True, handle_torrent_hashs except Exception as e: logger.error(f"删种失败: {str(e)}") return False, False, 0 - def __del_seed(self, download, download_id, action_flag, handle_torrent_hashs): + def __del_collection(self, src: str, delete_flag: bool, torrent_hash: str, download_files: list, + handle_torrent_hashs: list): + """ + 处理合集 + """ + try: + download_file = self._downloadhis.get_file_by_fullpath(fullpath=src) + # src查询记录 判断download_hash是否不一致 + if download_file and str(download_file.download_hash) != str(torrent_hash): + # 查询新download_hash对应files数量 + hash_download_files = self._downloadhis.get_files_by_hash( + download_hash=download_file.download_hash) + # 新download_hash对应files数量 > 删种download_hash对应files数量 = 合集种子 + if hash_download_files \ + and len(hash_download_files) > len(download_files) \ + and hash_download_files[0].id > download_files[-1].id: + # 查询未删除数 + no_del_cnt = 0 + for hash_download_file in hash_download_files: + if hash_download_file and hash_download_file.state and int( + hash_download_file.state) == 1: + no_del_cnt += 1 + if no_del_cnt > 0: + logger.info(f"合集种子 {download_file.download_hash} 文件未完全删除,执行暂停种子操作") + delete_flag = False + + # 删除合集种子 + if delete_flag: + if str(download_file.downloader) == "transmission": + self.tr.delete_torrents(delete_file=True, + ids=download_file.download_hash) + else: + self.qb.delete_torrents(delete_file=True, + ids=download_file.download_hash) + + logger.info(f"删除合集种子 {download_file.downloader} {download_file.download_hash}") + else: + # 暂停合集种子 + if str(download_file.downloader) == "transmission": + self.tr.stop_torrents(ids=download_file.download_hash) + else: + self.qb.stop_torrents(ids=download_file.download_hash) + logger.info(f"暂停合集种子 {download_file.downloader} {download_file.download_hash}") + # 已处理种子+1 + handle_torrent_hashs.append(download_file.download_hash) + + # 处理合集辅种 + handle_torrent_hashs = self.__del_seed(download=download_file.downloader, + download_id=download_file.download_hash, + delete_flag=delete_flag, + handle_torrent_hashs=handle_torrent_hashs) + except Exception as e: + logger.error(f"处理 {torrent_hash} 合集失败") + print(str(e)) + + return handle_torrent_hashs + + def __del_seed(self, download, download_id, delete_flag, handle_torrent_hashs): """ 删除辅种 """ @@ -1109,8 +1273,8 @@ class MediaSyncDel(_PluginBase): # 有辅种记录则处理辅种 if seed_history and isinstance(seed_history, list): for history in seed_history: - downloader = history['downloader'] - torrents = history['torrents'] + downloader = history.get("downloader") + torrents = history.get("torrents") if not downloader or not torrents: return if not isinstance(torrents, list): @@ -1121,28 +1285,33 @@ class MediaSyncDel(_PluginBase): handle_torrent_hashs.append(torrent) if str(download) == "qbittorrent": # 删除辅种 - if action_flag == "del": + if delete_flag: logger.info(f"删除辅种:{downloader} - {torrent}") self.qb.delete_torrents(delete_file=True, ids=torrent) # 暂停辅种 - if action_flag == "stop": - self.qb.stop_torrents(torrent) + else: + self.qb.stop_torrents(ids=torrent) logger.info(f"辅种:{downloader} - {torrent} 暂停") else: # 删除辅种 - if action_flag == "del": + if delete_flag: logger.info(f"删除辅种:{downloader} - {torrent}") self.tr.delete_torrents(delete_file=True, ids=torrent) # 暂停辅种 - if action_flag == "stop": - self.tr.stop_torrents(torrent) + else: + self.tr.stop_torrents(ids=torrent) logger.info(f"辅种:{downloader} - {torrent} 暂停") - break + + # 处理辅种的辅种 + handle_torrent_hashs = self.__del_seed(download=downloader, + download_id=torrent, + delete_flag=delete_flag, + handle_torrent_hashs=handle_torrent_hashs) # 删除辅种历史 - if action_flag == "del": + if delete_flag: self.del_data(key=history_key, plugin_id=plugin_id) return handle_torrent_hashs @@ -1153,7 +1322,7 @@ class MediaSyncDel(_PluginBase): 获取emby日志列表、解析emby日志 """ - def __parse_log(file_name: str, del_list: list): + def __parse_log(file_name: str, del_list: list, last_time): """ 解析emby日志 """ @@ -1246,7 +1415,9 @@ class MediaSyncDel(_PluginBase): del_medias = [] log_files.reverse() for log_file in log_files: - del_medias = __parse_log(log_file, del_medias) + del_medias = __parse_log(file_name=log_file, + del_list=del_medias, + last_time=last_time) return del_medias @@ -1256,7 +1427,7 @@ class MediaSyncDel(_PluginBase): 获取jellyfin日志列表、解析jellyfin日志 """ - def __parse_log(file_name: str, del_list: list): + def __parse_log(file_name: str, del_list: list, last_time): """ 解析jellyfin日志 """ @@ -1349,7 +1520,9 @@ class MediaSyncDel(_PluginBase): del_medias = [] log_files.reverse() for log_file in log_files: - del_medias = __parse_log(log_file, del_medias) + del_medias = __parse_log(file_name=log_file, + del_list=del_medias, + last_time=last_time) return del_medias @@ -1383,7 +1556,8 @@ class MediaSyncDel(_PluginBase): # 查询下载hash download_hash = self._downloadhis.get_hash_by_fullpath(src) if download_hash: - self.handle_torrent(src=src, torrent_hash=download_hash) + download_history = self._downloadhis.get_by_hash(download_hash) + self.handle_torrent(type=download_history.type, src=src, torrent_hash=download_hash) else: logger.warn(f"未查询到文件 {src} 对应的下载记录") diff --git a/plugins/messageforward/__init__.py b/plugins/messageforward/__init__.py index 4351dd3..6fcbd77 100644 --- a/plugins/messageforward/__init__.py +++ b/plugins/messageforward/__init__.py @@ -69,81 +69,102 @@ class MessageForward(_PluginBase): 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 """ return [ - { - 'component': 'VForm', - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enabled', - 'label': '开启转发' - } - } - ] - }, - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'wechat', - 'rows': '3', - 'label': '应用配置', - 'placeholder': 'appid:corpid:appsecret(一行一个配置)' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'pattern', - 'rows': '3', - 'label': '正则配置', - 'placeholder': '对应上方应用配置,一行一个,一一对应' - } - } - ] - } - ] - }, - ] - } - ], { - "enabled": False, - "wechat": "", - "pattern": "" - } + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '开启转发' + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'wechat', + 'rows': '3', + 'label': '应用配置', + 'placeholder': 'appid:corpid:appsecret(一行一个配置)' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'pattern', + 'rows': '3', + 'label': '正则配置', + 'placeholder': '对应上方应用配置,一行一个,一一对应' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '根据正则表达式,把MoviePilot的消息转发到多个微信应用。' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "wechat": "", + "pattern": "" + } def get_page(self) -> List[dict]: pass