diff --git a/app/db/downloadhistory_oper.py b/app/db/downloadhistory_oper.py index ee1e6e80..1e02ff31 100644 --- a/app/db/downloadhistory_oper.py +++ b/app/db/downloadhistory_oper.py @@ -42,3 +42,16 @@ class DownloadHistoryOper(DbOper): 清空下载记录 """ DownloadHistory.truncate(self._db) + + def get_last_by(self, mtype=None, title: str = None, year: str = None, + season: str = None, episode: str = None, tmdbid=None) -> DownloadHistory: + """ + 按类型、标题、年份、季集查询下载记录 + """ + return DownloadHistory.get_last_by(db=self._db, + mtype=mtype, + title=title, + year=year, + season=season, + episode=episode, + tmdbid=tmdbid) diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index 979bdec1..9d472350 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -49,3 +49,43 @@ class DownloadHistory(Base): @staticmethod def get_by_path(db: Session, path: str): return db.query(DownloadHistory).filter(DownloadHistory.path == path).first() + + @staticmethod + def get_last_by(db: Session, mtype: str = None, title: str = None, year: int = None, season: str = None, + episode: str = None, tmdbid: str = None): + """ + 据tmdbid、season、season_episode查询转移记录 + """ + if tmdbid and not season and not episode: + return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).order_by( + DownloadHistory.id.desc()).first() + if tmdbid and season and not episode: + return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, + DownloadHistory.seasons == season).order_by( + DownloadHistory.id.desc()).first() + if tmdbid and season and episode: + return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, + DownloadHistory.seasons == season, + DownloadHistory.episodes == episode).order_by( + DownloadHistory.id.desc()).first() + # 电视剧所有季集|电影 + if not season and not episode: + return db.query(DownloadHistory).filter(DownloadHistory.type == mtype, + DownloadHistory.title == title, + DownloadHistory.year == year).order_by( + DownloadHistory.id.desc()).first() + # 电视剧某季 + if season and not episode: + return db.query(DownloadHistory).filter(DownloadHistory.type == mtype, + DownloadHistory.title == title, + DownloadHistory.year == year, + DownloadHistory.seasons == season).order_by( + DownloadHistory.id.desc()).first() + # 电视剧某季某集 + if season and episode: + return db.query(DownloadHistory).filter(DownloadHistory.type == mtype, + DownloadHistory.title == title, + DownloadHistory.year == year, + DownloadHistory.seasons == season, + DownloadHistory.episodes == episode).order_by( + DownloadHistory.id.desc()).first() diff --git a/app/db/transferhistory_oper.py b/app/db/transferhistory_oper.py index 3825dfb0..456f5b30 100644 --- a/app/db/transferhistory_oper.py +++ b/app/db/transferhistory_oper.py @@ -50,7 +50,7 @@ class TransferHistoryOper(DbOper): """ return TransferHistory.statistic(self._db, days) - def get_by(self, mtype: str = None, title: str = None, year: int = None, + def get_by(self, mtype: str = None, title: str = None, year: str = None, season: str = None, episode: str = None, tmdbid: str = None) -> Any: """ 按类型、标题、年份、季集查询转移记录 diff --git a/app/plugins/dirmonitor/__init__.py b/app/plugins/dirmonitor/__init__.py index 582b56a7..1c79a1e6 100644 --- a/app/plugins/dirmonitor/__init__.py +++ b/app/plugins/dirmonitor/__init__.py @@ -12,6 +12,7 @@ from app.chain.transfer import TransferChain from app.core.config import settings from app.core.context import MediaInfo from app.core.metainfo import MetaInfo +from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.transferhistory_oper import TransferHistoryOper from app.log import logger from app.plugins import _PluginBase @@ -65,6 +66,7 @@ class DirMonitor(_PluginBase): # 私有属性 transferhis = None + downloadhis = None transferchian = None _observer = [] _enabled = False @@ -78,6 +80,7 @@ class DirMonitor(_PluginBase): def init_plugin(self, config: dict = None): self.transferhis = TransferHistoryOper() + self.downloadhis = DownloadHistoryOper() self.transferchian = TransferChain() # 读取配置 @@ -121,8 +124,9 @@ class DirMonitor(_PluginBase): except Exception as e: err_msg = str(e) if "inotify" in err_msg and "reached" in err_msg: - logger.warn(f"目录监控服务启动出现异常:{err_msg},请在宿主机上(不是docker容器内)执行以下命令并重启:" - + """ + logger.warn( + f"目录监控服务启动出现异常:{err_msg},请在宿主机上(不是docker容器内)执行以下命令并重启:" + + """ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf sudo sysctl -p @@ -226,6 +230,13 @@ class DirMonitor(_PluginBase): )) return + # 获取downloadhash + downloadHis = self.downloadhis.get_last_by(mtype=mediainfo.type.value, + title=mediainfo.title, + year=mediainfo.year, + season=file_meta.season, + episode=file_meta.episode, + tmdbid=mediainfo.tmdb_id) # 新增转移成功历史记录 self.transferhis.add( src=event_path, @@ -242,6 +253,7 @@ class DirMonitor(_PluginBase): seasons=file_meta.season, episodes=file_meta.episode, image=mediainfo.get_poster_image(), + download_hash=downloadHis.download_hash if downloadHis else None, status=1 ) @@ -251,7 +263,8 @@ class DirMonitor(_PluginBase): self.chain.refresh_mediaserver(mediainfo=mediainfo, file_path=transferinfo.target_path) # 发送通知 if self._notify: - self.transferchian.send_transfer_message(meta=file_meta, mediainfo=mediainfo, transferinfo=transferinfo) + self.transferchian.send_transfer_message(meta=file_meta, mediainfo=mediainfo, + transferinfo=transferinfo) # 广播事件 self.eventmanager.send_event(EventType.TransferComplete, { 'meta': file_meta, @@ -274,147 +287,147 @@ class DirMonitor(_PluginBase): def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 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': 'VSelect', - 'props': { - 'model': 'mode', - 'label': '监控模式', - 'items': [ - {'title': '兼容模式', 'value': 'compatibility'}, - {'title': '性能模式', 'value': 'fast'} - ] - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VSelect', - 'props': { - 'model': 'transfer_type', - 'label': '转移方式', - 'items': [ - {'title': '移动', 'value': 'move'}, - {'title': '复制', 'value': 'copy'}, - {'title': '硬链接', 'value': 'link'}, - {'title': '软链接', 'value': 'softlink'} - ] - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12 - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'monitor_dirs', - 'label': '监控目录', - 'rows': 5, - 'placeholder': '每一行一个目录' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12 - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'exclude_keywords', - 'label': '排除关键词', - 'rows': 2, - 'placeholder': '每一行一个关键词' - } - } - ] - } - ] - } - ] - } - ], { - "enabled": False, - "notify": False, - "mode": "fast", - "transfer_type": settings.TRANSFER_TYPE, - "monitor_dirs": "", - "exclude_keywords": "" - } + { + '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': 'VSelect', + 'props': { + 'model': 'mode', + 'label': '监控模式', + 'items': [ + {'title': '兼容模式', 'value': 'compatibility'}, + {'title': '性能模式', 'value': 'fast'} + ] + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'transfer_type', + 'label': '转移方式', + 'items': [ + {'title': '移动', 'value': 'move'}, + {'title': '复制', 'value': 'copy'}, + {'title': '硬链接', 'value': 'link'}, + {'title': '软链接', 'value': 'softlink'} + ] + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'monitor_dirs', + 'label': '监控目录', + 'rows': 5, + 'placeholder': '每一行一个目录' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'exclude_keywords', + 'label': '排除关键词', + 'rows': 2, + 'placeholder': '每一行一个关键词' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "notify": False, + "mode": "fast", + "transfer_type": settings.TRANSFER_TYPE, + "monitor_dirs": "", + "exclude_keywords": "" + } def get_page(self) -> List[dict]: pass diff --git a/app/plugins/mediasyncdel/__init__.py b/app/plugins/mediasyncdel/__init__.py index c081a53c..93389113 100644 --- a/app/plugins/mediasyncdel/__init__.py +++ b/app/plugins/mediasyncdel/__init__.py @@ -112,150 +112,150 @@ class MediaSyncDel(_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': 'del_source', - 'label': '删除源文件', - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VSelect', - 'props': { - 'model': 'sync_type', - 'label': '同步方式', - 'items': [ - {'title': '日志', 'value': 'log'}, - {'title': 'Scripter X', 'value': 'plugin'} - ] - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'cron', - 'label': '执行周期', - 'placeholder': '5位cron表达式,留空自动' - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'exclude_path', - 'label': '排除路径' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'text': '同步方式分为日志同步和Scripter X。日志同步需要配置执行周期,默认30分钟执行一次。' - 'Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。' - } - } - ] - } - ] - } - ] - } - ], { - "enabled": False, - "notify": True, - "del_source": False, - "sync_type": "log", - "cron": "*/30 * * * *", - "exclude_path": "", - } + { + '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': 'del_source', + 'label': '删除源文件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'sync_type', + 'label': '同步方式', + 'items': [ + {'title': '日志', 'value': 'log'}, + {'title': 'Scripter X', 'value': 'plugin'} + ] + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'exclude_path', + 'label': '排除路径' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'text': '同步方式分为日志同步和Scripter X。日志同步需要配置执行周期,默认30分钟执行一次。' + 'Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "notify": True, + "del_source": False, + "sync_type": "log", + "cron": "*/30 * * * *", + "exclude_path": "", + } def get_page(self) -> List[dict]: """ @@ -286,6 +286,83 @@ class MediaSyncDel(_PluginBase): 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', @@ -314,58 +391,7 @@ class MediaSyncDel(_PluginBase): }, { 'component': 'div', - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'pa-0 px-2' - }, - 'text': f'类型:{htype}' - }, - { - '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}' - } - ] + 'content': sub_contents } ] } @@ -510,9 +536,11 @@ class MediaSyncDel(_PluginBase): if self._notify: if media_type == "Episode": # 根据tmdbid获取图片 - image = self.episode.images(tv_id=tmdb_id, - season_num=season_num, - episode_num=episode_num) + images = self.episode.images(tv_id=tmdb_id, + season_num=season_num, + episode_num=episode_num) + if images: + image = self.get_tmdbimage_url(images[-1].get("file_path"), prefix="original") # 发送通知 self.post_message( mtype=NotificationType.MediaServer, @@ -530,7 +558,7 @@ class MediaSyncDel(_PluginBase): "season": season_num, "episode": episode_num, "image": image, - "del_time": str(datetime.datetime.now()) + "del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) }) # 保存历史 @@ -671,7 +699,7 @@ class MediaSyncDel(_PluginBase): "season": media_season, "episode": media_episode, "image": image, - "del_time": del_time + "del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) }) # 保存历史 @@ -1034,3 +1062,10 @@ class MediaSyncDel(_PluginBase): if event: self.post_message(channel=event.event_data.get("channel"), title="媒体库同步删除完成!", userid=event.event_data.get("user")) + + @staticmethod + def get_tmdbimage_url(path, prefix="w500"): + if not path: + return "" + tmdb_image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}" + return tmdb_image_url + f"/t/p/{prefix}{path}"