Files
archived-MoviePilot-Plugins/plugins.v2/mediasyncdel/__init__.py

1252 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import shutil
import time
from pathlib import Path
from typing import List, Tuple, Dict, Any, Optional
from apscheduler.schedulers.background import BackgroundScheduler
from app import schemas
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.core.config import settings
from app.core.event import eventmanager, Event
from app.db.models.transferhistory import TransferHistory
from app.db.transferhistory_oper import TransferHistoryOper
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.helper.downloader import DownloaderHelper
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import NotificationType, EventType, MediaType, MediaImageType
from app.utils.system import SystemUtils
class MediaSyncDel(_PluginBase):
# 插件名称
plugin_name = "媒体文件同步删除"
# 插件描述
plugin_desc = "同步删除历史记录、源文件和下载任务。"
# 插件图标
plugin_icon = "mediasyncdel.png"
# 插件版本
plugin_version = "1.9.1"
# 插件作者
plugin_author = "thsrite"
# 作者主页
author_url = "https://github.com/thsrite"
# 插件配置项ID前缀
plugin_config_prefix = "mediasyncdel_"
# 加载顺序
plugin_order = 9
# 可使用的用户级别
auth_level = 1
# 私有属性
_scheduler: Optional[BackgroundScheduler] = None
_enabled = False
_sync_type: str = ""
_notify = False
_del_source = False
_del_history = False
_exclude_path = None
_library_path = None
_transferchain = None
_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 = TransferHistoryOper()
self._downloadhis = DownloadHistoryOper()
self._storagechain = StorageChain()
# 读取配置
if config:
self._enabled = config.get("enabled")
self._sync_type = config.get("sync_type")
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")
# 获取默认下载器
downloader_services = self._downloader_helper.get_services()
for downloader_name, downloader_info in downloader_services.items():
if downloader_info.config.default:
self._default_downloader = downloader_name
# 清理插件历史
if self._del_history:
self.del_data(key="history")
self.update_config({
"enabled": self._enabled,
"sync_type": self._sync_type,
"notify": self._notify,
"del_source": self._del_source,
"del_history": False,
"exclude_path": self._exclude_path,
"library_path": self._library_path
})
@staticmethod
def get_command() -> List[Dict[str, Any]]:
"""
定义远程控制命令
:return: 命令关键字、事件、描述、附带数据
"""
pass
def get_api(self) -> List[Dict[str, Any]]:
return [
{
"path": "/delete_history",
"endpoint": self.delete_history,
"methods": ["GET"],
"summary": "删除订阅历史记录"
}
]
def delete_history(self, key: str, apikey: str):
"""
删除历史记录
"""
if apikey != settings.API_TOKEN:
return schemas.Response(success=False, message="API密钥错误")
# 历史记录
historys = self.get_data('history')
if not historys:
return schemas.Response(success=False, message="未找到历史记录")
# 删除指定记录
historys = [h for h in historys if h.get("unique") != key]
self.save_data('history', historys)
return schemas.Response(success=True, message="删除成功")
def get_service(self) -> List[Dict[str, Any]]:
"""
注册插件公共服务
[{
"id": "服务ID",
"name": "服务名称",
"trigger": "触发器cron/interval/date/CronTrigger.from_crontab()",
"func": self.xxx,
"kwargs": {} # 定时器参数
}]
"""
return []
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': 'del_source',
'label': '删除源文件',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'del_history',
'label': '删除历史',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSelect',
'props': {
'model': 'sync_type',
'label': '媒体库同步方式',
'items': [
{'title': 'Webhook', 'value': 'webhook'},
{'title': 'Scripter X', 'value': 'plugin'}
]
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 8
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'exclude_path',
'label': '排除路径'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VTextarea',
'props': {
'model': 'library_path',
'rows': '2',
'label': '媒体库路径映射',
'placeholder': '媒体服务器路径:MoviePilot路径一行一个'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '媒体库同步方式分为Webhook、Scripter X'
'1、Webhook需要Emby4.8.0.45及以上开启媒体删除的Webhook。'
'2、Scripter X方式需要emby安装并配置Scripter X插件无需配置执行周期。'
'3、启用该插件后非媒体服务器触发的源文件删除也会同步处理下载器中的下载任务。'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '关于路径映射(转移后文件路径):'
'emby:/data/A.mp4,'
'moviepilot:/mnt/link/A.mp4。'
'路径映射填/data:/mnt/link。'
'不正确配置会导致查询不到转移记录!(路径一样可不填)'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '排除路径:命中排除路径后请求云盘删除插件删除云盘资源。'
}
}
]
}
]
},
{
'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'
}
}
]
}
]
}
]
}
], {
"enabled": False,
"notify": True,
"del_source": False,
"del_history": False,
"library_path": "",
"sync_type": "webhook",
"exclude_path": "",
}
def get_page(self) -> List[dict]:
"""
拼装插件详情页面,需要返回页面配置,同时附带数据
"""
# 查询同步详情
historys = self.get_data('history')
if not historys:
return [
{
'component': 'div',
'text': '暂无数据',
'props': {
'class': 'text-center',
}
}
]
# 数据按时间降序排序
historys = sorted(historys, key=lambda x: x.get('del_time'), reverse=True)
# 拼装页面
contents = []
for history in historys:
htype = history.get("type")
title = history.get("title")
unique = history.get("unique")
year = history.get("year")
season = history.get("season")
episode = history.get("episode")
image = history.get("image")
del_time = history.get("del_time")
if season:
sub_contents = [
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'类型:{htype}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'标题:{title}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'年份:{year}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'季:{season}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'集:{episode}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'时间:{del_time}'
}
]
else:
sub_contents = [
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'类型:{htype}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'标题:{title}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'年份:{year}'
},
{
'component': 'VCardText',
'props': {
'class': 'pa-0 px-2'
},
'text': f'时间:{del_time}'
}
]
contents.append(
{
'component': 'VCard',
'content': [
{
"component": "VDialogCloseBtn",
"props": {
'innerClass': 'absolute top-0 right-0',
},
'events': {
'click': {
'api': 'plugin/MediaSyncDel/delete_history',
'method': 'get',
'params': {
'key': unique,
'apikey': settings.API_TOKEN
}
}
},
},
{
'component': 'div',
'props': {
'class': 'd-flex justify-space-start flex-nowrap flex-row',
},
'content': [
{
'component': 'div',
'content': [
{
'component': 'VImg',
'props': {
'src': image,
'height': 120,
'width': 80,
'aspect-ratio': '2/3',
'class': 'object-cover shadow ring-gray-500',
'cover': True
}
}
]
},
{
'component': 'div',
'content': sub_contents
}
]
}
]
}
)
return [
{
'component': 'div',
'props': {
'class': 'grid gap-3 grid-info-card',
},
'content': contents
}
]
@eventmanager.register(EventType.WebhookMessage)
def sync_del_by_webhook(self, event: Event):
"""
emby删除媒体库同步删除历史记录
webhook
"""
if not self._enabled or str(self._sync_type) != "webhook":
return
event_data: schemas.WebhookEventInfo = event.event_data
event_type = event_data.event
# Emby Webhook event_type = library.deleted
if not event_type or str(event_type) not in ['library.deleted', 'ItemDeleted']:
return
# 媒体类型
media_type = event_data.media_type
# 媒体名称
media_name = event_data.item_name
# 媒体路径
media_path = event_data.item_path
# tmdb_id
tmdb_id = event_data.tmdb_id
# 季数
season_num = event_data.season_id
# 集数
episode_num = event_data.episode_id
# 操作时间戳
delete_time = self.format_timestamp(event_data.json_object)
"""
执行删除逻辑
"""
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.PluginAction,
{
"action": "networkdisk_del",
"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请检查媒体库媒体是否刮削")
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,
delete_time=delete_time)
@eventmanager.register(EventType.WebhookMessage)
def sync_del_by_plugin(self, event: Event):
"""
emby删除媒体库同步删除历史记录
Scripter X插件
"""
if not self._enabled or str(self._sync_type) != "plugin":
return
event_data: schemas.WebhookEventInfo = event.event_data
event_type = event_data.event
# Scripter X插件 event_type = media_del
if not event_type or str(event_type) != 'media_del':
return
# Scripter X插件 需要是否虚拟标识
item_isvirtual = event_data.item_isvirtual
if not item_isvirtual:
logger.error("Scripter X插件方式item_isvirtual参数未配置为防止误删除暂停插件运行")
self.update_config({
"enabled": False,
"del_source": self._del_source,
"exclude_path": self._exclude_path,
"library_path": self._library_path,
"notify": self._notify,
"sync_type": self._sync_type,
})
return
# 如果是虚拟item则直接return不进行删除
if item_isvirtual == 'True':
return
# 媒体类型
media_type = event_data.item_type
# 媒体名称
media_name = event_data.item_name
# 媒体路径
media_path = event_data.item_path
# tmdb_id
tmdb_id = event_data.tmdb_id
# 季数
season_num = event_data.season_id
# 集数
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.PluginAction,
{
"action": "networkdisk_del",
"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
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)
@eventmanager.register(EventType.PluginAction)
def sync_del(self, event: Event = None):
"""
扫描
"""
if not self._enabled or not event:
return
event_data = event.event_data
if not event_data or event_data.get("action") != "media_sync_del":
return
logger.info(f"收到媒体同步删除请求:{event_data}")
# 媒体类型
media_type = event_data.get("media_type")
# 媒体名称
media_name = event_data.get("media_name")
# 媒体路径
media_path = event_data.get("media_path")
# tmdb_id
tmdb_id = event_data.get("tmdb_id")
# 季数
season_num = event_data.get("season_num")
# 集数
episode_num = event_data.get("episode_num")
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,
delete_time: Optional[str] = None,
):
if not media_type:
logger.error(f"{media_name} 同步删除失败,未获取到媒体类型,请检查媒体是否刮削")
return
# 处理路径映射 (处理同一媒体多分辨率的情况)
if self._library_path:
paths = self._library_path.split("\n")
for path in paths:
sub_paths = path.split(":")
if len(sub_paths) < 2:
continue
media_path = media_path.replace(sub_paths[0], sub_paths[1]).replace('\\', '/')
# 兼容重新整理的场景
if Path(media_path).exists():
logger.warn(f"转移路径 {media_path} 未被删除或重新生成,跳过处理")
return
# 查询转移记录
msg, transfer_history = self.__get_transfer_his(media_type=media_type,
media_name=media_name,
media_path=media_path,
tmdb_id=tmdb_id,
season_num=season_num,
episode_num=episode_num)
if not transfer_history:
logger.warn(
f"{media_type} {media_name} 未获取到可删除数据请检查路径映射是否配置错误请检查tmdbid获取是否正确")
return
if delete_time:
latest_his = max(transfer_history, key=lambda x: x.date)
if delete_time < latest_his.date:
logger.warn(f"忽略删除 {msg}, 整理时间: {latest_his.date}, 发生在删除事件: {delete_time} 之后")
return
logger.info(f"开始同步删除 {msg}, 获取到 {len(transfer_history)} 条转移记录")
# 开始删除
year = None
del_torrent_hashs = []
stop_torrent_hashs = []
error_cnt = 0
image = 'https://emby.media/notificationicon.png'
for transferhis in transfer_history:
title = transferhis.title
if title not in media_name:
logger.warn(
f"当前转移记录 {transferhis.id} {title} {transferhis.tmdbid} 与删除媒体{media_name}不符,防误删,暂不自动删除")
continue
image = transferhis.image or image
year = transferhis.year
# 0、删除转移记录
self._transferhis.delete(transferhis.id)
# 删除种子任务
if self._del_source:
# 1、直接删除源文件
if transferhis.src and Path(transferhis.src).suffix in settings.RMT_MEDIAEXT:
# 删除硬链接文件和源文件
if Path(transferhis.dest).exists():
Path(transferhis.dest).unlink(missing_ok=True)
self.__remove_parent_dir(Path(transferhis.dest))
if Path(transferhis.src).exists():
logger.info(f"源文件 {transferhis.src} 开始删除")
Path(transferhis.src).unlink(missing_ok=True)
logger.info(f"源文件 {transferhis.src} 已删除")
self.__remove_parent_dir(Path(transferhis.src))
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:
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} 完成!")
media_type = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV
# 发送消息
if self._notify:
backrop_image = self.chain.obtain_specific_image(
mediaid=tmdb_id,
mtype=media_type,
image_type=MediaImageType.Backdrop,
season=season_num,
episode=episode_num
) or image
torrent_cnt_msg = ""
if del_torrent_hashs:
torrent_cnt_msg += f"删除种子{len(set(del_torrent_hashs))}\n"
if stop_torrent_hashs:
stop_cnt = 0
# 排除已删除
for stop_hash in set(stop_torrent_hashs):
if stop_hash not in set(del_torrent_hashs):
stop_cnt += 1
if stop_cnt > 0:
torrent_cnt_msg += f"暂停种子{stop_cnt}\n"
if error_cnt:
torrent_cnt_msg += f"删种失败{error_cnt}\n"
# 发送通知
self.post_message(
mtype=NotificationType.Plugin,
title="媒体库同步删除任务完成",
image=backrop_image,
text=f"{msg}\n"
f"删除记录{len(transfer_history)}\n"
f"{torrent_cnt_msg}"
f"时间 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}"
)
# 读取历史记录
history = self.get_data('history') or []
# 获取poster
poster_image = self.chain.obtain_specific_image(
mediaid=tmdb_id,
mtype=media_type,
image_type=MediaImageType.Poster,
) or image
history.append({
"type": media_type.value,
"title": media_name,
"year": year,
"path": media_path,
"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())),
"unique": f"{media_name}:{tmdb_id}:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}"
})
# 保存历史
self.save_data("history", history)
def __remove_parent_dir(self, file_path: Path):
"""
删除父目录
"""
# 删除空目录
# 判断当前媒体父路径下是否有媒体文件,如有则无需遍历父级
if not SystemUtils.exits_files(file_path.parent, settings.RMT_MEDIAEXT):
# 判断父目录是否为空, 为空则删除
i = 0
for parent_path in file_path.parents:
i += 1
if i > 3:
break
if str(parent_path.parent) != str(file_path.root):
# 父目录非根目录,才删除父目录
if not SystemUtils.exits_files(parent_path, settings.RMT_MEDIAEXT):
# 当前路径下没有媒体文件则删除
shutil.rmtree(parent_path)
logger.warn(f"本地空目录 {parent_path} 已删除")
def __get_transfer_his(self, media_type: str, media_name: str, media_path: str,
tmdb_id: int, season_num: str, episode_num: str):
"""
查询转移记录
"""
# 季数
if season_num and str(season_num).isdigit():
season_num = str(season_num).rjust(2, '0')
else:
season_num = None
# 集数
if episode_num and str(episode_num).isdigit():
episode_num = str(episode_num).rjust(2, '0')
else:
episode_num = None
# 类型
mtype = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV
# 删除电影
if mtype == MediaType.MOVIE:
msg = f'电影 {media_name} {tmdb_id}'
transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id,
mtype=mtype.value,
dest=media_path)
# 删除电视剧
elif mtype == MediaType.TV and not season_num and not episode_num:
msg = f'剧集 {media_name} {tmdb_id}'
transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id,
mtype=mtype.value)
# 删除季 S02
elif mtype == MediaType.TV and season_num and not episode_num:
if not season_num or not str(season_num).isdigit():
logger.error(f"{media_name} 季同步删除失败,未获取到具体季")
return
msg = f'剧集 {media_name} S{season_num} {tmdb_id}'
if tmdb_id and str(tmdb_id).isdigit():
# 根据tmdb_id查询转移记录
transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id,
mtype=mtype.value,
season=f'S{season_num}')
else:
# 兼容emby webhook不发送tmdb场景
transfer_history: List[TransferHistory] = self._transferhis.get_by(mtype=mtype.value,
season=f'S{season_num}',
dest=media_path)
# 删除剧集S02E02
elif mtype == MediaType.TV and season_num and episode_num:
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,
mtype=mtype.value,
season=f'S{season_num}',
episode=f'E{episode_num}',
dest=media_path)
else:
return "", []
return msg, transfer_history
def handle_torrent(self, type: str, src: str, torrent_hash: str):
"""
判断种子是否局部删除
局部删除则暂停种子
全部删除则删除种子
"""
download_id = torrent_hash
download = self._default_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}")
handle_torrent_hashs = []
try:
# 删除本次种子记录
self._downloadhis.delete_file_by_fullpath(fullpath=src)
# 根据种子hash查询所有下载器文件记录
download_files = self._downloadhis.get_files_by_hash(download_hash=torrent_hash)
if not download_files:
logger.error(
f"未查询到种子任务 {torrent_hash} 存在文件记录,未执行下载器文件同步或该种子已被删除")
return False, False, 0
# 查询未删除数
no_del_cnt = 0
for download_file in download_files:
if download_file and download_file.state and int(download_file.state) == 1:
no_del_cnt += 1
if no_del_cnt > 0:
logger.info(
f"查询种子任务 {torrent_hash} 存在 {no_del_cnt} 个未删除文件,执行暂停种子操作")
delete_flag = False
else:
logger.info(
f"查询种子任务 {torrent_hash} 文件已全部删除,执行删除种子操作")
delete_flag = True
# 如果有转种记录,则删除转种后的下载任务
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']
# 删除种子
if delete_flag:
# 删除转种记录
self.del_data(key=history_key, plugin_id=plugin_id)
# 转种后未删除源种时,同步删除源种
if not delete_source:
logger.info(f"{history_key} 转种时未删除源下载任务,开始删除源下载任务…")
# 删除源种子
logger.info(f"删除源下载器下载任务:{self._default_downloader} - {torrent_hash}")
self.chain.remove_torrents(torrent_hash)
handle_torrent_hashs.append(torrent_hash)
# 删除转种后任务
logger.info(f"删除转种后下载任务:{download} - {download_id}")
# 删除转种后下载任务
self.chain.remove_torrents(hashs=torrent_hash,
downloader=download)
handle_torrent_hashs.append(download_id)
else:
# 暂停种子
# 转种后未删除源种时,同步暂停源种
if not delete_source:
logger.info(f"{history_key} 转种时未删除源下载任务,开始暂停源下载任务…")
# 暂停源种子
logger.info(f"暂停源下载器下载任务:{self._default_downloader} - {torrent_hash}")
self.chain.stop_torrents(torrent_hash)
handle_torrent_hashs.append(torrent_hash)
logger.info(f"暂停转种后下载任务:{download} - {download_id}")
# 删除转种后下载任务
self.chain.stop_torrents(hashs=download_id, downloader=download)
handle_torrent_hashs.append(download_id)
else:
# 未转种de情况
if delete_flag:
# 删除源种子
logger.info(f"删除源下载器下载任务:{download} - {download_id}")
self.chain.remove_torrents(download_id)
else:
# 暂停源种子
logger.info(f"暂停源下载器下载任务:{download} - {download_id}")
self.chain.stop_torrents(download_id)
handle_torrent_hashs.append(download_id)
# 处理辅种
handle_torrent_hashs = self.__del_seed(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_collection(self, src: str, delete_flag: bool, torrent_hash: str, download_files: list,
handle_torrent_hashs: list):
"""
处理合集
"""
try:
src_download_files = self._downloadhis.get_files_by_fullpath(fullpath=src)
if src_download_files:
for download_file in src_download_files:
# src查询记录 判断download_hash是否不一致
if download_file and download_file.download_hash 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:
self.chain.remove_torrents(hashs=download_file.download_hash,
downloader=download_file.downloader)
logger.info(f"删除合集种子 {download_file.downloader} {download_file.download_hash}")
else:
# 暂停合集种子
self.chain.stop_torrents(hashs=download_file.download_hash,
downloader=download_file.downloader)
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_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_id, delete_flag, handle_torrent_hashs):
"""
删除辅种
"""
# 查询是否有辅种记录
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.get("downloader")
torrents = history.get("torrents")
if not downloader or not torrents:
return
if not isinstance(torrents, list):
torrents = [torrents]
# 删除辅种历史
for torrent in torrents:
handle_torrent_hashs.append(torrent)
# 删除辅种
if delete_flag:
logger.info(f"删除辅种:{downloader} - {torrent}")
self.chain.remove_torrents(hashs=torrent,
downloader=downloader)
# 暂停辅种
else:
self.chain.stop_torrents(hashs=torrent, downloader=downloader)
logger.info(f"辅种:{downloader} - {torrent} 暂停")
# 处理辅种的辅种
handle_torrent_hashs = self.__del_seed(download_id=torrent,
delete_flag=delete_flag,
handle_torrent_hashs=handle_torrent_hashs)
# 删除辅种历史
if delete_flag:
self.del_data(key=history_key,
plugin_id=plugin_id)
return handle_torrent_hashs
def get_state(self):
return self._enabled
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))
@eventmanager.register(EventType.DownloadFileDeleted)
def downloadfile_del_sync(self, event: Event):
"""
下载文件删除处理事件
"""
if not event:
return
event_data = event.event_data
src = event_data.get("src")
if not src:
return
# 查询下载hash
download_hash = self._downloadhis.get_hash_by_fullpath(src)
if 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} 对应的下载记录")
@staticmethod
def get_tmdbimage_url(path: str, prefix="w500"):
if not path:
return ""
tmdb_image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}"
return tmdb_image_url + f"/t/p/{prefix}{path}"
@staticmethod
def format_timestamp(json_data: dict) -> str:
from app.utils.string import StringUtils
return StringUtils.format_timestamp(
StringUtils.str_to_timestamp(
json_data.get("UtcTimestamp") # jellyfin
or json_data.get("Date") # emby
)
)