diff --git a/app/chain/__init__.py b/app/chain/__init__.py index cd6b3c3c..c934fe75 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -2,6 +2,7 @@ import gc import pickle import traceback from abc import ABCMeta +from msilib.schema import File from pathlib import Path from typing import Optional, Any, Tuple, List, Set, Union, Dict @@ -307,6 +308,14 @@ class ChainBase(metaclass=ABCMeta): """ return self.run_module("stop_torrents", hashs=hashs) + def torrent_files(self, tid: str) -> Optional[List[File]]: + """ + 根据种子文件,选择并添加下载任务 + :param tid: 种子Hash + :return: 种子文件 + """ + return self.run_module("torrent_files", tid=tid) + def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: """ 判断媒体文件是否存在 diff --git a/app/chain/download.py b/app/chain/download.py index 417998c1..4a007af0 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -1,4 +1,5 @@ import re +from msilib.schema import File from pathlib import Path from typing import List, Optional, Tuple, Set, Dict, Union @@ -623,3 +624,13 @@ class DownloadChain(ChainBase): 删除下载任务 """ return self.remove_torrents(hashs=[hash_str]) + + def get_files(self, tid: str) -> Optional[List[File]]: + """ + 获取种子文件清单 + """ + try: + return self.torrent_files(tid=tid) + except Exception as err: + logger.error(f"获取种子文件列表出错:{err}") + return None diff --git a/app/db/models/plugin.py b/app/db/models/plugin.py index 485a6446..e81b5e2c 100644 --- a/app/db/models/plugin.py +++ b/app/db/models/plugin.py @@ -20,3 +20,7 @@ class PluginData(Base): @staticmethod def get_plugin_data_by_key(db: Session, plugin_id: str, key: str): return db.query(PluginData).filter(PluginData.plugin_id == plugin_id, PluginData.key == key).first() + + @staticmethod + def del_plugin_data_by_key(db: Session, plugin_id: str, key: str): + return db.query(PluginData).filter(PluginData.plugin_id == plugin_id, PluginData.key == key).delete() diff --git a/app/db/plugindata_oper.py b/app/db/plugindata_oper.py index c60befb6..9fce2ce4 100644 --- a/app/db/plugindata_oper.py +++ b/app/db/plugindata_oper.py @@ -44,6 +44,14 @@ class PluginDataOper(DbOper): return json.loads(data.value) return data.value + def del_data(self, plugin_id: str, key: str) -> Any: + """ + 删除插件数据 + :param plugin_id: 插件id + :param key: 数据key + """ + PluginData.del_plugin_data_by_key(self._db, plugin_id, key) + def truncate(self): """ 清空插件数据 diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 6a6b649b..6663d8c8 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -1,3 +1,4 @@ +from msilib.schema import File from pathlib import Path from typing import Set, Tuple, Optional, Union, List @@ -187,6 +188,12 @@ class QbittorrentModule(_ModuleBase): """ return self.qbittorrent.start_torrents(ids=hashs) + def torrent_files(self, tid: str) -> Optional[List[File]]: + """ + 获取种子文件列表 + """ + return self.qbittorrent.get_files(tid=tid) + def downloader_info(self) -> schemas.DownloaderInfo: """ 下载器信息 diff --git a/app/modules/qbittorrent/qbittorrent.py b/app/modules/qbittorrent/qbittorrent.py index 3b33e1e6..09c7d67f 100644 --- a/app/modules/qbittorrent/qbittorrent.py +++ b/app/modules/qbittorrent/qbittorrent.py @@ -1,8 +1,9 @@ import time +from msilib.schema import File from typing import Optional, Union, Tuple, List import qbittorrentapi -from qbittorrentapi import TorrentFilesList, TorrentDictionary +from qbittorrentapi import TorrentDictionary from qbittorrentapi.client import Client from qbittorrentapi.transfer import TransferInfoDictionary @@ -265,7 +266,7 @@ class Qbittorrent(metaclass=Singleton): logger.error(f"删除种子出错:{err}") return False - def get_files(self, tid: str) -> Optional[TorrentFilesList]: + def get_files(self, tid: str) -> Optional[List[File]]: """ 获取种子文件清单 """ diff --git a/app/modules/transmission/__init__.py b/app/modules/transmission/__init__.py index e05939d9..1a8b17a4 100644 --- a/app/modules/transmission/__init__.py +++ b/app/modules/transmission/__init__.py @@ -1,3 +1,4 @@ +from msilib.schema import File from pathlib import Path from typing import Set, Tuple, Optional, Union, List @@ -171,6 +172,12 @@ class TransmissionModule(_ModuleBase): """ return self.transmission.start_torrents(ids=hashs) + def torrent_files(self, tid: str) -> Optional[List[File]]: + """ + 获取种子文件列表 + """ + return self.transmission.get_files(tid=tid) + def downloader_info(self) -> schemas.DownloaderInfo: """ 下载器信息 diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py index caeda644..fafc32d6 100644 --- a/app/plugins/__init__.py +++ b/app/plugins/__init__.py @@ -142,20 +142,35 @@ class _PluginBase(metaclass=ABCMeta): data_path.mkdir(parents=True) return data_path - def save_data(self, key: str, value: Any) -> Base: + def save_data(self, key: str, value: Any, plugin_id: str = None) -> Base: """ 保存插件数据 :param key: 数据key :param value: 数据值 """ - return self.plugindata.save(self.__class__.__name__, key, value) + if not plugin_id: + plugin_id = self.__class__.__name__ + return self.plugindata.save(plugin_id, key, value) - def get_data(self, key: str) -> Any: + def get_data(self, key: str, plugin_id: str = None) -> Any: """ 获取插件数据 :param key: 数据key + :param plugin_id: plugin_id """ - return self.plugindata.get_data(self.__class__.__name__, key) + if not plugin_id: + plugin_id = self.__class__.__name__ + return self.plugindata.get_data(plugin_id, key) + + def del_data(self, key: str, plugin_id: str = None) -> Any: + """ + 删除插件数据 + :param key: 数据key + :param plugin_id: plugin_id + """ + if not plugin_id: + plugin_id = self.__class__.__name__ + return self.plugindata.del_data(plugin_id, key) def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None, text: str = None, image: str = None, link: str = None, userid: str = None): diff --git a/app/plugins/mediasyncdel/__init__.py b/app/plugins/mediasyncdel/__init__.py index f23e18f2..02b98fb4 100644 --- a/app/plugins/mediasyncdel/__init__.py +++ b/app/plugins/mediasyncdel/__init__.py @@ -4,6 +4,7 @@ import os import re import shutil import time +from pathlib import Path from typing import List, Tuple, Dict, Any, Optional from apscheduler.schedulers.background import BackgroundScheduler @@ -461,17 +462,14 @@ class MediaSyncDel(_PluginBase): logger.error(f"{media_name} 季同步删除失败,未获取到具体季") return msg = f'剧集 {media_name} S{season_num} {tmdb_id}' - transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id, - season=f"S{season_num}") + transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id) # 删除剧集S02E02 elif media_type == "Episode": if not season_num or not str(season_num).isdigit() or not episode_num or not str(episode_num).isdigit(): logger.error(f"{media_name} 集同步删除失败,未获取到具体集") return msg = f'剧集 {media_name} S{season_num}E{episode_num} {tmdb_id}' - transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id, - season=f"S{season_num}", - episode=f"E{episode_num}") + transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id) else: return @@ -482,24 +480,20 @@ class MediaSyncDel(_PluginBase): return # 开始删除 - del_cnt = 0 image = 'https://emby.media/notificationicon.png' year = None for transferhis in transfer_history: image = transferhis.image year = transferhis.year - if media_type == "Episode" or media_type == "Movie": - # 如果有剧集或者电影有多个版本的话,需要根据名称筛选下要删除的版本 - if os.path.basename(transferhis.dest) != os.path.basename(media_path): - continue - self._transferhis.delete(transferhis.id) - del_cnt += 1 # 删除种子任务 if self._del_source: del_source = False if transferhis.download_hash: try: - self.chain.remove_torrents(transferhis.download_hash) + # 判断种子是否被删除完 + self.handle_torrent(history_id=transferhis.id, + src=history.src, + torrent_hash=history.download_hash) except Exception as e: logger.error("删除种子失败,尝试删除源文件:%s" % str(e)) del_source = True @@ -527,7 +521,6 @@ class MediaSyncDel(_PluginBase): title="媒体库同步删除任务完成", image=image, text=f"{msg}\n" - f"数量 {del_cnt}\n" f"时间 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}" ) @@ -616,17 +609,14 @@ class MediaSyncDel(_PluginBase): transfer_history: List[TransferHistory] = self._transferhis.get_by( mtype="电视剧", title=media_name, - year=media_year, - season=media_season) + year=media_year) # 删除剧集S02E02 elif media_type == "Episode": msg = f'剧集 {media_name} {media_season}{media_episode}' transfer_history: List[TransferHistory] = self._transferhis.get_by( mtype="电视剧", title=media_name, - year=media_year, - season=media_season, - episode=media_episode) + year=media_year) else: continue @@ -648,7 +638,10 @@ class MediaSyncDel(_PluginBase): del_source = False if transferhis.download_hash: try: - self.chain.remove_torrents(transferhis.download_hash) + # 判断种子是否被删除完 + self.handle_torrent(history_id=transferhis.id, + src=history.src, + torrent_hash=history.download_hash) except Exception as e: logger.error("删除种子失败,尝试删除源文件:%s" % str(e)) del_source = True @@ -688,6 +681,149 @@ class MediaSyncDel(_PluginBase): self.save_data("last_time", datetime.datetime.now()) + def handle_torrent(self, history_id: int, src: str, torrent_hash: str): + """ + 判断种子是否局部删除 + 局部删除则暂停种子 + 全部删除则删除种子 + """ + download_id = torrent_hash + download = settings.DOWNLOADER + history_key = "%s-%s" % (download, torrent_hash) + plugin_id = "TorrentTransfer" + transfer_history = self.get_data(key=history_key, + plugin_id=plugin_id) + logger.info(f"查询到 {history_key} 转种历史 {transfer_history}") + + # 删除历史标志 + del_history = False + # 删除种子标志 + delete_flag = True + + # 是否需要暂停源下载器种子 + stop_from = False + + # 如果有转种记录,则删除转种后的下载任务 + if transfer_history and isinstance(transfer_history, dict): + download = transfer_history['to_download'] + download_id = transfer_history['to_download_id'] + delete_source = transfer_history['delete_source'] + del_history = True + + # 转种后未删除源种时,同步删除源种 + if not delete_source: + logger.info(f"{history_key} 转种时未删除源下载任务,开始删除源下载任务…") + + try: + dl_files = self.chain.get_files(tid=torrent_hash) + if not dl_files: + logger.info(f"未获取到 {settings.DOWNLOADER} - {torrent_hash} 种子文件,种子已被删除") + else: + for dl_file in dl_files: + dl_file_name = dl_file.get("name") + torrent_file = os.path.join(src, os.path.basename(dl_file_name)) + if Path(torrent_file).exists(): + logger.warn(f"种子有文件被删除,种子文件{torrent_file}暂未删除,暂停种子") + delete_flag = False + stop_from = True + break + if delete_flag: + logger.info(f"删除下载任务:{settings.DOWNLOADER} - {torrent_hash}") + self.chain.remove_torrents(torrent_hash) + except Exception as e: + logger.error(f"删除源下载任务 {history_key} 失败: {str(e)}") + + # 如果是False则说明种子文件没有完全被删除,暂停种子,暂不处理 + if delete_flag: + try: + dl_files = self.chain.get_files(tid=download_id) + if not dl_files: + logger.info(f"未获取到 {download} - {download_id} 种子文件,种子已被删除") + else: + for dl_file in dl_files: + dl_file_name = dl_file.get("name") + if not stop_from: + torrent_file = os.path.join(src, os.path.basename(dl_file_name)) + if Path(torrent_file).exists(): + logger.info(f"种子有文件被删除,种子文件{torrent_file}暂未删除,暂停种子") + delete_flag = False + break + if delete_flag: + # 删除源下载任务或转种后下载任务 + logger.info(f"删除下载任务:{download} - {download_id}") + self.chain.remove_torrents(download_id) + + # 删除转移记录 + self._transferhis.delete(history_id) + + # 删除转种记录 + if del_history: + self.del_data(key=history_key, plugin_id=plugin_id) + + # 处理辅种 + self.__del_seed(download=download, download_id=download_id, action_flag="del") + except Exception as e: + logger.error(f"删除转种辅种下载任务失败: {str(e)}") + + # 判断是否暂停 + if not delete_flag: + logger.error("开始暂停种子") + # 暂停种子 + if stop_from: + # 暂停源种 + self.chain.stop_torrents(torrent_hash) + logger.info(f"种子:{settings.DOWNLOADER} - {torrent_hash} 暂停") + + # 转种 + self.chain.stop_torrents(download_id) + logger.info(f"转种:{download} - {download_id} 暂停") + + # 辅种 + self.__del_seed(download=download, download_id=download_id, action_flag="stop") + + def __del_seed(self, download, download_id, action_flag): + """ + 删除辅种 + """ + # 查询是否有辅种记录 + history_key = download_id + plugin_id = "IYUUAutoSeed" + seed_history = self.get_data(key=history_key, + plugin_id=plugin_id) or [] + logger.info(f"查询到 {history_key} 辅种历史 {seed_history}") + + # 有辅种记录则处理辅种 + if seed_history and isinstance(seed_history, list): + for history in seed_history: + downloader = history['downloader'] + torrents = history['torrents'] + if not downloader or not torrents: + return + if not isinstance(torrents, list): + torrents = [torrents] + + # 删除辅种历史中与本下载器相同的辅种记录 + if int(downloader) == download: + for torrent in torrents: + # 删除辅种 + if action_flag == "del": + logger.info(f"删除辅种:{downloader} - {torrent}") + self.chain.remove_torrents(torrent) + # 暂停辅种 + if action_flag == "stop": + self.chain.stop_torrents(torrent) + logger.info(f"辅种:{downloader} - {torrent} 暂停") + + # 删除本下载器辅种历史 + if action_flag == "del": + del history + break + + # 更新辅种历史 + self.save_data(key=history_key, + value=seed_history, + plugin_id=plugin_id) + @staticmethod def parse_emby_log(last_time): log_url = "{HOST}System/Logs/embyserver.txt?api_key={APIKEY}"