diff --git a/package.json b/package.json index 93c2bc9..c4876ba 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ "icon": "download.png", "author": "thsrite", "level": 1, - "v2": true, "history": { "v1.1": "支持选择MoviePilot配置的下载路径", "v1.0": "删除下载器中该站点辅种,保留该站点没有辅种的种子" diff --git a/package.v2.json b/package.v2.json index bc02888..9fe4893 100644 --- a/package.v2.json +++ b/package.v2.json @@ -176,11 +176,12 @@ "name": "媒体文件同步删除", "description": "同步删除历史记录、源文件和下载任务。", "labels": "媒体库,文件整理", - "version": "1.8.4", + "version": "1.8.6", "icon": "mediasyncdel.png", "author": "thsrite", "level": 1, "history": { + "v1.8.6": "修复删除源文件", "v1.8.4": "修复暂停种子失败", "v1.8.3": "修复源文件删除", "v1.8.1": "适配v2多媒体服务器,移除日志方式", @@ -193,11 +194,13 @@ "name": "下载器文件同步", "description": "同步下载器的文件信息到数据库,删除文件时联动删除下载任务。", "labels": "下载管理", - "version": "1.1.3", + "version": "1.1.6", "icon": "Youtube-dl_A.png", "author": "thsrite", "level": 1, "history": { + "v1.1.6": "添加记录默认状态", + "v1.1.5": "修复同步下载器种子", "v1.1.3": "支持v2", "v1.1.1": "修复时区问题导致的上次同步后8h内的种子不同步的问题" } @@ -400,11 +403,14 @@ "name": "云盘Strm助手", "description": "实时监控、定时全量增量生成strm文件。", "labels": "云盘", - "version": "1.0.6", + "version": "1.0.9", "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/cloudcompanion.png", "author": "thsrite", "level": 1, "history": { + "v1.0.9": "目录树支持多级结构", + "v1.0.8": "修复重建缓存不生效", + "v1.0.7": "修复复制非媒体文件时父目录不存在", "v1.0.6": "支持[目录实时监控]插件联动", "v1.0.5": "增加复制非媒体文件选项", "v1.0.4": "修复实时监控,只处理指定类型的文件", @@ -425,5 +431,19 @@ "history": { "v1.0": "重写Strm文件内容" } + }, + "DownloadTorrent": { + "name": "添加种子下载", + "description": "选择下载器,添加种子任务。", + "labels": "站点", + "version": "2.0", + "icon": "download.png", + "author": "thsrite", + "level": 1, + "history": { + "v2.0": "兼容V2版本", + "v1.1": "支持选择MoviePilot配置的下载路径", + "v1.0": "删除下载器中该站点辅种,保留该站点没有辅种的种子" + } } } diff --git a/plugins.v2/cloudstrmcompanion/__init__.py b/plugins.v2/cloudstrmcompanion/__init__.py index 5617d5d..eaa3483 100644 --- a/plugins.v2/cloudstrmcompanion/__init__.py +++ b/plugins.v2/cloudstrmcompanion/__init__.py @@ -58,7 +58,7 @@ class CloudStrmCompanion(_PluginBase): # 插件图标 plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/cloudcompanion.png" # 插件版本 - plugin_version = "1.0.6" + plugin_version = "1.0.9" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -131,6 +131,9 @@ class CloudStrmCompanion(_PluginBase): logger.info("开始清理旧数据索引") self._rebuild = False self._cloud_files = [] + if Path(self._cloud_files_json).exists(): + Path(self._cloud_files_json).unlink() + logger.info("旧数据索引清理完成") self.__update_config() # 停止现有任务 @@ -305,9 +308,9 @@ class CloudStrmCompanion(_PluginBase): if not tree_content: continue # 遍历云盘树形结构文件 - for cloud_file in self.parse_tree_structure(tree_content): + for cloud_file in self.parse_tree_structure(content=tree_content, dir_path=cloud_dir): try: - if Path(cloud_file).is_dir(): + if Path(str(cloud_file)).is_dir(): continue # 本地挂载路径 local_file = str(cloud_file).replace(cloud_dir, local_dir) @@ -396,6 +399,8 @@ class CloudStrmCompanion(_PluginBase): strm_content=strm_content) else: if self._copy_files: + # 确保目标文件的父目录存在 + os.makedirs(os.path.dirname(target_file), exist_ok=True) # 其他nfo、jpg等复制文件 shutil.copy2(str(event_path), target_file) logger.info(f"复制其他文件 {str(event_path)} 到 {target_file}") @@ -539,24 +544,26 @@ class CloudStrmCompanion(_PluginBase): return None @staticmethod - def parse_tree_structure(content: str): + def parse_tree_structure(content: str, dir_path: str): """ 解析目录树内容并生成每个路径 """ tree_pattern = re_compile(r"^(?:\| )+\|-") - current_path = ["/"] # 初始化当前路径为根目录 + dir_path = Path(dir_path) + current_path = [str(dir_path.parent)] if dir_path.parent != Path("/") or (dir_path.parent == dir_path and ( + dir_path.is_absolute() or ':' in dir_path.name)) else ["/"] # 初始化当前路径为根目录 for line in content.splitlines(): # 匹配目录树的每一行 match = tree_pattern.match(line) - if not match or "根目录" in line: + if not match: continue # 跳过不符合格式的行 # 计算当前行的深度 level_indicator = match.group(0) depth = (len(level_indicator) // 2) - 1 - # 获取当前行的目录名称 - item_name = escape(line.strip()[len(level_indicator):]) + # 获取当前行的目录名称,去掉前面的 '| ' 或 '- ' + item_name = escape(line.strip()[len(level_indicator):].strip()) # 根据深度更新当前路径 if depth < len(current_path): @@ -565,7 +572,7 @@ class CloudStrmCompanion(_PluginBase): current_path.append(item_name) # 添加新的深度名称 # 生成并返回当前深度的完整路径 - yield join_path(*current_path[:depth + 1]) + yield join_path(*current_path[:depth + 1]).replace('\\', '/') @eventmanager.register(EventType.PluginAction) def remote_sync_one(self, event: Event = None): diff --git a/plugins.v2/downloadtorrent/__init__.py b/plugins.v2/downloadtorrent/__init__.py new file mode 100644 index 0000000..c42042f --- /dev/null +++ b/plugins.v2/downloadtorrent/__init__.py @@ -0,0 +1,314 @@ +from typing import Any, List, Dict, Tuple, Optional +from app.db.site_oper import SiteOper +from app.plugins import _PluginBase +from app.log import logger +from app.utils.string import StringUtils +from app.schemas import ServiceInfo +from app.helper.downloader import DownloaderHelper +from app.helper.directory import DirectoryHelper + + + +class DownloadTorrent(_PluginBase): + # 插件名称 + plugin_name = "添加种子下载" + # 插件描述 + plugin_desc = "选择下载器,添加种子任务。" + # 插件图标 + plugin_icon = "download.png" + # 插件版本 + plugin_version = "2.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "downloadtorrent_" + # 加载顺序 + plugin_order = 28 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _is_paused = False + _save_path = None + _mp_path = None + _downloader = None + site = None + torrent_helper = None + downloader_helper = None + directory_helper = None + + def init_plugin(self, config: dict = None): + self.downloader_helper = DownloaderHelper() + self.directory_helper = DirectoryHelper() + self.site = SiteOper() + + if config: + self._is_paused = config.get("is_paused") + self._save_path = config.get("save_path") + self._mp_path = config.get("mp_path") + self._torrent_urls = config.get("torrent_urls") + self._downloader = config.get("downloader") + + # 下载种子 + if self._torrent_urls: + for torrent_url in str(self._torrent_urls).split("\n"): + # 获取种子对应站点cookie + domain = StringUtils.get_url_domain(torrent_url) + if not domain: + logger.error(f"种子 {torrent_url} 获取站点域名失败,跳过处理") + continue + + # 查询站点 + site = self.site.get_by_domain(domain) + if not site or not site.cookie: + logger.error(f"种子 {torrent_url} 获取站点cookie失败,跳过处理") + continue + + service = self.service_info(self._downloader) + download_id = self.__download(service=service, + content=torrent_url, + save_path=self._save_path or self._mp_path, + cookie=site.cookie) + + if download_id: + logger.info(f"种子添加下载成功 {torrent_url} 保存位置 {self._save_path or self._mp_path}") + else: + logger.error(f"种子添加下载失败 {torrent_url} 保存位置 {self._save_path or self._mp_path}") + + self.update_config({ + "downloader": self._downloader, + "save_path": self._save_path, + "mp_path": self._mp_path, + "is_paused": self._is_paused + }) + + def service_info(self, name: str) -> Optional[ServiceInfo]: + """ + 服务信息 + """ + if not name: + logger.warning("尚未配置下载器,请检查配置") + return None + + service = self.downloader_helper.get_service(name) + if not service or not service.instance: + logger.warning(f"获取下载器 {name} 实例失败,请检查配置") + return None + + if service.instance.is_inactive(): + logger.warning(f"下载器 {name} 未连接,请检查配置") + return None + return service + + def __download(self, service: ServiceInfo, content: bytes, + save_path: str, cookie: str) -> Optional[str]: + """ + 添加下载任务 + """ + if not service or not service.instance: + return + downloader = service.instance + if self.downloader_helper.is_downloader("qbittorrent", service=service): + torrent = downloader.add_torrent(content=content, + download_dir=save_path, + is_paused=self._is_paused, + cookie=cookie) + if not torrent: + return None + else: + return torrent + elif self.downloader_helper.is_downloader("transmission", service=service): + # 添加任务 + torrent = downloader.add_torrent(content=content, + download_dir=save_path, + is_paused=self._is_paused, + cookie=cookie) + if not torrent: + return None + else: + return torrent.hashString + + logger.error(f"不支持的下载器类型") + return None + + + def get_state(self) -> bool: + return False + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + dir_conf = [{'title': d.name, 'value': d.download_path} for d in self.directory_helper.get_local_download_dirs()] + downloader_options = [{"title": config.name, "value": config.name} for config in self.downloader_helper.get_configs().values()] + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'downloader', + 'label': '下载器', + 'items': downloader_options + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'is_paused', + 'label': '暂停种子', + 'items': [ + {'title': '开启', 'value': True}, + {'title': '不开启', 'value': False} + ] + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'mp_path', + 'label': 'MoviePilot保存路径', + 'items': dir_conf + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'save_path', + 'label': '自定义保存路径' + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'torrent_urls', + 'rows': '3', + 'label': '种子链接', + 'placeholder': '种子链接,一行一个' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '自定义保存路径优先级高于MoviePilot保存路径。' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '保存路径为下载器保存路径,种子链接一行一个。' + '添加的种子链接需站点已在站点管理维护或公共站点。' + } + } + ] + } + ] + } + ] + } + ], { + "downloader": "qb", + "is_paused": False, + "save_path": "", + "mp_path": "", + "torrent_urls": "" + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + pass \ No newline at end of file diff --git a/plugins.v2/mediasyncdel/__init__.py b/plugins.v2/mediasyncdel/__init__.py index aec45e2..3171ac6 100644 --- a/plugins.v2/mediasyncdel/__init__.py +++ b/plugins.v2/mediasyncdel/__init__.py @@ -1,4 +1,3 @@ -import json import os import time from pathlib import Path @@ -26,7 +25,7 @@ class MediaSyncDel(_PluginBase): # 插件图标 plugin_icon = "mediasyncdel.png" # 插件版本 - plugin_version = "1.8.4" + plugin_version = "1.8.6" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -51,12 +50,14 @@ class MediaSyncDel(_PluginBase): _transferhis = None _downloadhis = None _default_downloader = None + _storagechain = None def init_plugin(self, config: dict = None): self._transferchain = TransferChain() self._downloader_helper = DownloaderHelper() self._transferhis = self._transferchain.transferhis self._downloadhis = self._transferchain.downloadhis + self._storagechain = StorageChain() # 读取配置 if config: @@ -656,7 +657,6 @@ class MediaSyncDel(_PluginBase): "exclude_path": self._exclude_path, "library_path": self._library_path, "notify": self._notify, - "cron": self._cron, "sync_type": self._sync_type, }) return @@ -767,24 +767,32 @@ class MediaSyncDel(_PluginBase): if self._del_source: # 1、直接删除源文件 if transferhis.src and Path(transferhis.src).suffix in settings.RMT_MEDIAEXT: - dest_fileitem = schemas.FileItem(**transferhis.dest_fileitem) - state = StorageChain().delete_file(dest_fileitem) - if state and transferhis.download_hash: - 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: - error_cnt += 1 - else: - if delete_flag: - del_torrent_hashs += handle_torrent_hashs + self._storagechain.delete_file(schemas.FileItem(**transferhis.dest_fileitem)) + src_fileitem = schemas.FileItem(**transferhis.src_fileitem) + logger.info(f"开始删除源文件 {src_fileitem.path}") + state = self._storagechain.delete_file(src_fileitem) + if state: + folder_item = self._storagechain.get_parent_item(src_fileitem) + if folder_item and not self._storagechain.any_files(folder_item, + extensions=settings.RMT_MEDIAEXT): + logger.warn(f"删除残留空文件夹:【{folder_item.storage}】{folder_item.path}") + self._storagechain.delete_file(folder_item) + if transferhis.download_hash: + 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: + error_cnt += 1 else: - stop_torrent_hashs += handle_torrent_hashs - except Exception as e: - logger.error("删除种子失败:%s" % str(e)) + if delete_flag: + del_torrent_hashs += handle_torrent_hashs + else: + stop_torrent_hashs += handle_torrent_hashs + except Exception as e: + logger.error("删除种子失败:%s" % str(e)) logger.info(f"同步删除 {msg} 完成!") diff --git a/plugins.v2/syncdownloadfiles/__init__.py b/plugins.v2/syncdownloadfiles/__init__.py index 04959f9..ce483d8 100644 --- a/plugins.v2/syncdownloadfiles/__init__.py +++ b/plugins.v2/syncdownloadfiles/__init__.py @@ -22,7 +22,7 @@ class SyncDownloadFiles(_PluginBase): # 插件图标 plugin_icon = "Youtube-dl_A.png" # 插件版本 - plugin_version = "1.1.3" + plugin_version = "1.1.6" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -113,19 +113,20 @@ class SyncDownloadFiles(_PluginBase): continue # 把种子按照名称和种子大小分组,获取添加时间最早的一个,认定为是源种子,其余为辅种 - torrents = self.__get_origin_torrents(torrents, downloader) + downloader_config = self.__get_downloader_config(downloader) + torrents = self.__get_origin_torrents(torrents, downloader_config.type) logger.info(f"下载器 {downloader} 去除辅种,获取到源种子数:{len(torrents)}") for torrent in torrents: # 返回false,标识后续种子已被同步 - sync_flag = self.__compare_time(torrent, downloader, last_sync_time) + sync_flag = self.__compare_time(torrent, downloader_config.type, last_sync_time) if not sync_flag: logger.info(f"最后同步时间{last_sync_time}, 之前种子已被同步,结束当前下载器 {downloader} 任务") break # 获取种子hash - hash_str = self.__get_hash(torrent, downloader) + hash_str = self.__get_hash(torrent, downloader_config.type) # 判断是否是mp下载,判断download_hash是否在downloadhistory表中,是则不处理 downloadhis = self.downloadhis.get_by_hash(hash_str) @@ -136,7 +137,7 @@ class SyncDownloadFiles(_PluginBase): continue # 获取种子download_dir - download_dir = self.__get_download_dir(torrent, downloader) + download_dir = self.__get_download_dir(torrent, downloader_config.type) # 处理路径映射 if self._dirs: @@ -146,20 +147,20 @@ class SyncDownloadFiles(_PluginBase): download_dir = download_dir.replace(sub_paths[0], sub_paths[1]).replace('\\', '/') # 获取种子name - torrent_name = self.__get_torrent_name(torrent, downloader) + torrent_name = self.__get_torrent_name(torrent, downloader_config.type) # 种子保存目录 save_path = Path(download_dir).joinpath(torrent_name) # 获取种子文件 - torrent_files = self.__get_torrent_files(torrent, downloader, downloader_obj) + torrent_files = self.__get_torrent_files(torrent, downloader_config.type, downloader_obj) logger.info(f"开始同步种子 {hash_str}, 文件数 {len(torrent_files)}") download_files = [] for file in torrent_files: # 过滤掉没下载的文件 - if not self.__is_download(file, downloader): + if not self.__is_download(file, downloader_config.type): continue # 种子文件路径 - file_path_str = self.__get_file_path(file, downloader) + file_path_str = self.__get_file_path(file, downloader_config.type) file_path = Path(file_path_str) # 只处理视频格式 if not file_path.suffix \ @@ -190,6 +191,7 @@ class SyncDownloadFiles(_PluginBase): "savepath": str(save_path), "filepath": rel_path, "torrentname": torrent_name, + "state": 1 } )