feat 媒体库同步删除插件
This commit is contained in:
@ -77,3 +77,27 @@ class TransferHistory(Base):
|
||||
@staticmethod
|
||||
def count_by_title(db: Session, title: str):
|
||||
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.title.like(f'%{title}%')).first()[0]
|
||||
|
||||
@staticmethod
|
||||
def list_by(db: Session, mtype: str, title: str, year: int, season=None, episode=None):
|
||||
"""
|
||||
据tmdbid、season、season_episode查询转移记录
|
||||
"""
|
||||
# 电视剧所有季集|电影
|
||||
if not season and not episode:
|
||||
return db.query(TransferHistory).filter(TransferHistory.type == mtype,
|
||||
TransferHistory.title == title,
|
||||
TransferHistory.year == year).all()
|
||||
# 电视剧某季
|
||||
if season and not episode:
|
||||
return db.query(TransferHistory).filter(TransferHistory.type == mtype,
|
||||
TransferHistory.title == title,
|
||||
TransferHistory.year == year,
|
||||
TransferHistory.seasons == season).all()
|
||||
# 电视剧某季某集
|
||||
if season and episode:
|
||||
return db.query(TransferHistory).filter(TransferHistory.type == mtype,
|
||||
TransferHistory.title == title,
|
||||
TransferHistory.year == year,
|
||||
TransferHistory.seasons == season,
|
||||
TransferHistory.episodes == episode).all()
|
||||
|
@ -36,3 +36,19 @@ class TransferHistoryOper(DbOper):
|
||||
"""
|
||||
return TransferHistory.statistic(self._db, days)
|
||||
|
||||
def get_by(self, mtype: str, title: str, year: int, season=None, episode=None) -> Any:
|
||||
"""
|
||||
按类型、标题、年份、季集查询转移记录
|
||||
"""
|
||||
return TransferHistory.list_by(db=self._db,
|
||||
mtype=mtype,
|
||||
title=title,
|
||||
year=year,
|
||||
season=season,
|
||||
episode=episode)
|
||||
|
||||
def delete(self, historyid):
|
||||
"""
|
||||
删除转移记录
|
||||
"""
|
||||
TransferHistory.delete(self._db, historyid)
|
||||
|
@ -1,17 +1,27 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from typing import List, Tuple, Dict, Any
|
||||
import re
|
||||
import time
|
||||
from typing import List, Tuple, Dict, Any, Optional
|
||||
|
||||
from app.core.event import eventmanager
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import EventType
|
||||
from app.schemas.types import NotificationType, EventType
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class MediaSyncDel(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "Emby同步删除"
|
||||
plugin_name = "媒体库同步删除"
|
||||
# 插件描述
|
||||
plugin_desc = "Emby删除媒体后同步删除历史记录或源文件。"
|
||||
plugin_desc = "媒体库删除媒体后同步删除历史记录或源文件。"
|
||||
# 插件图标
|
||||
plugin_icon = "emby.png"
|
||||
# 主题色
|
||||
@ -30,127 +40,625 @@ class MediaSyncDel(_PluginBase):
|
||||
auth_level = 1
|
||||
|
||||
# 私有属性
|
||||
filetransfer = None
|
||||
_enable = False
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
_enabled = False
|
||||
_cron: str = ""
|
||||
_notify = False
|
||||
_del_source = False
|
||||
_exclude_path = None
|
||||
_send_notify = False
|
||||
|
||||
_transferhis = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self._transferhis = TransferHistoryOper()
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 读取配置
|
||||
if config:
|
||||
self._enable = config.get("enable")
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._notify = config.get("notify")
|
||||
self._del_source = config.get("del_source")
|
||||
self._exclude_path = config.get("exclude_path")
|
||||
self._send_notify = config.get("send_notify")
|
||||
|
||||
if self._enabled:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.sync_del,
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="媒体库同步删除")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
# 推送实时消息
|
||||
self.systemmessage.put(f"执行周期配置错误:{err}")
|
||||
else:
|
||||
self._scheduler.add_job(self.sync_del, "interval", minutes=30, name="媒体库同步删除")
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
"""
|
||||
定义远程控制命令
|
||||
:return: 命令关键字、事件、描述、附带数据
|
||||
"""
|
||||
return [{
|
||||
"cmd": "/sync_del",
|
||||
"event": EventType.HistoryDeleted,
|
||||
"desc": "媒体库同步删除",
|
||||
"data": {}
|
||||
}]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
pass
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'del_source',
|
||||
'label': '删除源文件',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'exclude_path',
|
||||
'label': '排除路径'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"del_source": False,
|
||||
"cron": "*/30 * * * *",
|
||||
"exclude_path": "",
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
# 查询同步详情
|
||||
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:
|
||||
type = history.get("type")
|
||||
title = history.get("title")
|
||||
year = history.get("year")
|
||||
season = history.get("season")
|
||||
episode = history.get("episode")
|
||||
image = history.get("image")
|
||||
del_time = history.get("del_time")
|
||||
|
||||
@eventmanager.register(EventType.WebhookMessage)
|
||||
def sync_del(self, event):
|
||||
contents.append(
|
||||
{
|
||||
'component': 'VCard',
|
||||
'content': [
|
||||
{
|
||||
'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': [
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'类型:{type}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardSubtitle',
|
||||
'props': {
|
||||
'class': 'pa-2 font-bold break-words whitespace-break-spaces'
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'a',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': 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}'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'grid gap-3 grid-info-card',
|
||||
},
|
||||
'content': contents
|
||||
}
|
||||
]
|
||||
|
||||
def sync_del(self):
|
||||
"""
|
||||
emby删除媒体库同步删除历史记录
|
||||
"""
|
||||
if not self._enable:
|
||||
return
|
||||
event_data = event.event_data
|
||||
event_type = event_data.get("event_type")
|
||||
if not event_type or str(event_type) != 'media_del':
|
||||
# 读取历史记录
|
||||
history = self.get_data('history') or []
|
||||
|
||||
# 媒体服务器类型
|
||||
media_server = settings.MEDIASERVER
|
||||
|
||||
last_time = self.get_data("last_time")
|
||||
del_medias = []
|
||||
if media_server == 'emby':
|
||||
del_medias = self.parse_emby_log(last_time)
|
||||
elif media_server == 'jellyfin':
|
||||
del_medias = self.parse_jellyfin_log(last_time)
|
||||
elif media_server == 'plex':
|
||||
# TODO plex解析日志
|
||||
return
|
||||
|
||||
# 是否虚拟标识
|
||||
item_isvirtual = event_data.get("item_isvirtual")
|
||||
if not item_isvirtual:
|
||||
logger.error("item_isvirtual参数未配置,为防止误删除,暂停插件运行")
|
||||
self.update_config({
|
||||
"enable": False,
|
||||
"del_source": self._del_source,
|
||||
"exclude_path": self._exclude_path,
|
||||
"send_notify": self._send_notify
|
||||
if not del_medias:
|
||||
logger.error("未解析到已删除媒体信息")
|
||||
return
|
||||
|
||||
# 遍历删除
|
||||
for del_media in del_medias:
|
||||
del_time = del_media.get("time")
|
||||
# 媒体类型 Movie|Series|Season|Episode
|
||||
media_type = del_media.get("type")
|
||||
# 媒体名称 蜀山战纪
|
||||
media_name = del_media.get("name")
|
||||
# 媒体年份 2015
|
||||
media_year = del_media.get("year")
|
||||
# 媒体路径 /data/series/国产剧/蜀山战纪 (2015)/Season 2/蜀山战纪 - S02E01 - 第1集.mp4
|
||||
media_path = del_media.get("path")
|
||||
# id 713083
|
||||
id = del_media.get("id")
|
||||
# 季数 S02
|
||||
media_season = del_media.get("season")
|
||||
# 集数 E02
|
||||
media_episode = del_media.get("episode")
|
||||
|
||||
# 排除路径不处理
|
||||
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} 已被排除,暂不处理")
|
||||
return
|
||||
|
||||
# 获取删除的记录
|
||||
# 删除电影
|
||||
if media_type == "Movie":
|
||||
msg = f'电影 {media_name}'
|
||||
transfer_history = self._transferhis.get_by(mtype="电影",
|
||||
title=media_name,
|
||||
year=media_year)
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# 删除电视剧
|
||||
elif media_type == "Series":
|
||||
msg = f'剧集 {media_name}'
|
||||
transfer_history = self._transferhis.get_by(mtype="电视剧",
|
||||
title=media_name,
|
||||
year=media_year)
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# 删除季 S02
|
||||
elif media_type == "Season":
|
||||
msg = f'剧集 {media_name} {media_season}'
|
||||
transfer_history = self._transferhis.get_by(mtype="电视剧",
|
||||
title=media_name,
|
||||
year=media_year,
|
||||
season=media_season)
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# 删除剧集S02E02
|
||||
elif media_type == "Episode":
|
||||
msg = f'剧集 {media_name} {media_season}{media_episode}'
|
||||
transfer_history = self._transferhis.get_by(mtype="电视剧",
|
||||
title=media_name,
|
||||
year=media_year,
|
||||
season=media_season,
|
||||
episode=media_episode)
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
else:
|
||||
continue
|
||||
|
||||
if not transfer_history:
|
||||
logger.info(f"未获取到 {msg} 转移记录")
|
||||
continue
|
||||
|
||||
logger.info(f"获取到删除媒体数量 {len(transfer_history)}")
|
||||
|
||||
# 开始删除
|
||||
image = 'https://emby.media/notificationicon.png'
|
||||
for transferhis in transfer_history:
|
||||
image = transferhis.image
|
||||
self._transferhis.delete(transferhis.id)
|
||||
|
||||
logger.info(f"同步删除 {msg} 完成!")
|
||||
|
||||
# 发送消息
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.MediaServer,
|
||||
title="Emby同步删除任务完成",
|
||||
text=f"{msg}\n"
|
||||
f"数量 {len(transfer_history)}\n"
|
||||
f"时间 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}",
|
||||
image=image)
|
||||
|
||||
history.append({
|
||||
"type": "电影" if media_type == "Movie" else "电视剧",
|
||||
"title": media_name,
|
||||
"year": media_year,
|
||||
"path": media_path,
|
||||
"season": media_season,
|
||||
"episode": media_episode,
|
||||
"image": image,
|
||||
"del_time": del_time
|
||||
})
|
||||
return
|
||||
|
||||
# 如果是虚拟item,则直接return,不进行删除
|
||||
if item_isvirtual == 'True':
|
||||
return
|
||||
# 保存历史
|
||||
self.save_data("history", history)
|
||||
|
||||
# 媒体类型
|
||||
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")
|
||||
if season_num and str(season_num).isdigit() and int(season_num) < 10:
|
||||
season_num = f'0{season_num}'
|
||||
# 集数
|
||||
episode_num = event_data.get("episode_num")
|
||||
if episode_num and str(episode_num).isdigit() and int(episode_num) < 10:
|
||||
episode_num = f'0{episode_num}'
|
||||
self.save_data("last_time", datetime.datetime.now())
|
||||
|
||||
if not media_type:
|
||||
logger.error(f"{media_name} 同步删除失败,未获取到媒体类型")
|
||||
return
|
||||
if not tmdb_id or not str(tmdb_id).isdigit():
|
||||
logger.error(f"{media_name} 同步删除失败,未获取到TMDB ID")
|
||||
return
|
||||
@staticmethod
|
||||
def parse_emby_log(last_time):
|
||||
# emby host
|
||||
emby_host = settings.EMBY_HOST
|
||||
if emby_host:
|
||||
if not emby_host.endswith("/"):
|
||||
emby_host += "/"
|
||||
if not emby_host.startswith("http"):
|
||||
emby_host = "http://" + emby_host
|
||||
|
||||
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} 已被排除,暂不处理")
|
||||
return
|
||||
# emby 日志url
|
||||
log_url = "%sSystem/Logs/embyserver.txt?api_key=%s" % (emby_host, settings.EMBY_API_KEY)
|
||||
log_res = RequestUtils().get_res(url=log_url)
|
||||
|
||||
# TODO 删除电影
|
||||
if media_type == "Movie":
|
||||
msg = f'电影 {media_name} {tmdb_id}'
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# TODO 删除电视剧
|
||||
elif media_type == "Series":
|
||||
msg = f'剧集 {media_name} {tmdb_id}'
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# TODO 删除季 S02
|
||||
elif media_type == "Season":
|
||||
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}'
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
# 删除剧集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}'
|
||||
logger.info(f"正在同步删除{msg}")
|
||||
else:
|
||||
return
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取emby日志失败,请检查服务器配置")
|
||||
return []
|
||||
|
||||
# TODO 开始删除
|
||||
# TODO 发送消息
|
||||
if self._send_notify:
|
||||
pass
|
||||
logger.info(f"同步删除 {msg} 完成!")
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}) Info App: Removing item from database, Type: (\w+), Name: (.*), Path: (.*), Id: (\d+)'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
|
||||
del_medias = []
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
time = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if last_time and time < last_time:
|
||||
continue
|
||||
|
||||
type = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
id = match[4]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if type == 'Episode' or type == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": time,
|
||||
"type": type,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"id": id,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_medias.append(media)
|
||||
|
||||
return del_medias
|
||||
|
||||
@staticmethod
|
||||
def parse_jellyfin_log(last_time):
|
||||
# jellyfin host
|
||||
jellyfin_host = settings.JELLYFIN_HOST
|
||||
if jellyfin_host:
|
||||
if not jellyfin_host.endswith("/"):
|
||||
jellyfin_host += "/"
|
||||
if not jellyfin_host.startswith("http"):
|
||||
jellyfin_host = "http://" + jellyfin_host
|
||||
|
||||
# jellyfin 日志url
|
||||
log_url = "%sSystem/Logs/jellyfinserver.txt?api_key=%s" % (jellyfin_host, settings.JELLYFIN_API_KEY)
|
||||
log_res = RequestUtils().get_res(url=log_url)
|
||||
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取jellyfin日志失败,请检查服务器配置")
|
||||
return []
|
||||
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}) Info App: Removing item from database, Type: (\w+), Name: (.*), Path: (.*), Id: (\d+)'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
|
||||
del_medias = []
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
time = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if time < last_time:
|
||||
continue
|
||||
|
||||
type = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
id = match[4]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if type == 'Episode' or type == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": time,
|
||||
"type": type,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"id": id,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_medias.append(media)
|
||||
|
||||
return del_medias
|
||||
|
||||
def get_state(self):
|
||||
return self._enable
|
||||
return self._enabled
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
pass
|
||||
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.HistoryDeleted)
|
||||
def remote_sync_del(self, event: Event):
|
||||
"""
|
||||
媒体库同步删除
|
||||
"""
|
||||
if event:
|
||||
logger.info("收到命令,开始执行媒体库同步删除 ...")
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="开始媒体库同步删除 ...",
|
||||
userid=event.event_data.get("user"))
|
||||
self.sync_del()
|
||||
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="媒体库同步删除完成!", userid=event.event_data.get("user"))
|
||||
|
Reference in New Issue
Block a user