From 3080bd516ed99bcfee42b38b8fc7b2be0c59582d Mon Sep 17 00:00:00 2001 From: thsrite Date: Wed, 28 Aug 2024 11:07:09 +0800 Subject: [PATCH] =?UTF-8?q?feat=20Emby=E5=89=A7=E9=9B=86=E6=BC=94=E5=91=98?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- icons/embyactorsync.png | Bin 0 -> 4451 bytes package.json | 12 ++ plugins/embyactorsync/__init__.py | 271 ++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 icons/embyactorsync.png create mode 100644 plugins/embyactorsync/__init__.py diff --git a/README.md b/README.md index 8fac457..dfcdd1c 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,5 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - 影视将映订阅 v1.1 - Emby视频类型检查 v1.0 - Emby有声书整理 v1.1 -- Emby弹幕下载 v1.2 \ No newline at end of file +- Emby弹幕下载 v1.2 +- Emby剧集演员同步 v1.0 \ No newline at end of file diff --git a/icons/embyactorsync.png b/icons/embyactorsync.png new file mode 100644 index 0000000000000000000000000000000000000000..bd09156b9410bd68dbeb3b7e275407521e8b2ce9 GIT binary patch literal 4451 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 Ygy=^;=dL#O%dQEap{lD=rDPlRA8Ga_ZvX%Q literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 522bb1b..ec340dc 100644 --- a/package.json +++ b/package.json @@ -758,5 +758,17 @@ "v1.1": "解析Emby日志,判断已配置弹幕源是否全部匹配失败。", "v1.0": "通知Emby Danmu插件下载弹幕。" } + }, + "EmbyActorSync": { + "name": "Emby剧集演员同步", + "description": "同步剧演员信息到集演员信息。", + "labels": "Emby,媒体库", + "version": "1.0", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/embyactorsync.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.0": "同步剧演员信息到集演员信息。" + } } } diff --git a/plugins/embyactorsync/__init__.py b/plugins/embyactorsync/__init__.py new file mode 100644 index 0000000..e4ee124 --- /dev/null +++ b/plugins/embyactorsync/__init__.py @@ -0,0 +1,271 @@ +import json +import time +from datetime import datetime, timedelta +from typing import Optional, Any, List, Dict, Tuple + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from app.core.config import settings +from app.log import logger +from app.plugins import _PluginBase +from app.modules.emby import Emby +from app.utils.http import RequestUtils + + +class EmbyActorSync(_PluginBase): + # 插件名称 + plugin_name = "Emby剧集演员同步" + # 插件描述 + plugin_desc = "同步剧演员信息到集演员信息。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/embyactorsync.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "embyactorsync_" + # 加载顺序 + plugin_order = 32 + # 可使用的用户级别 + auth_level = 1 + + _onlyonce = False + _librarys = None + _EMBY_HOST = settings.EMBY_HOST + _EMBY_USER = Emby().get_user() + _EMBY_APIKEY = settings.EMBY_API_KEY + _scheduler: Optional[BackgroundScheduler] = None + + def init_plugin(self, config: dict = None): + if config: + self._onlyonce = config.get("onlyonce") + self._librarys = config.get("librarys") or [] + + if self._EMBY_HOST: + if not self._EMBY_HOST.endswith("/"): + self._EMBY_HOST += "/" + if not self._EMBY_HOST.startswith("http"): + self._EMBY_HOST = "http://" + self._EMBY_HOST + + # 加载模块 + if self._onlyonce: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + # 立即运行一次 + if self._onlyonce: + logger.info(f"Emby剧集演员同步服务启动,立即运行一次") + self._scheduler.add_job(self.sync, 'date', + run_date=datetime.now( + tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="Emby剧集演员同步") + + # 关闭一次性开关 + self._onlyonce = False + + # 保存配置 + self.__update_config() + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def get_state(self) -> bool: + return False + + def __update_config(self): + self.update_config( + { + "onlyonce": self._onlyonce, + "librarys": self._librarys, + } + ) + + def sync(self): + """ + Emby剧集演员同步 + """ + # 获取媒体库信息 + librarys = Emby().get_librarys() + + # 匹配需要的媒体库 + for library in librarys: + if str(library.type) != "tvshows": + continue + if self._librarys and library.name not in self._librarys: + continue + logger.info(f"开始同步媒体库:{library.name},ID:{library.id}") + # 获取媒体库媒体列表 + library_items = self.__get_items(library.id) + if not library_items: + logger.error(f"获取媒体库:{library.name}的媒体列表失败") + continue + + # 遍历媒体列表,获取媒体的ID和名称 + for item in library_items: + item_info = self.__get_item_info(item.get("Id")) + seasons = self.__get_items(item.get("Id")) + for season in seasons: + season_items = self.__get_items(season.get("Id")) + for season_item in season_items: + retry = 0 + while retry < 3: + season_item_info = self.__get_item_info(season_item.get("Id")) + try: + if season_item_info.get("People") == item_info.get("People"): + logger.warn( + f"媒体:{item.get('Name')} {season_item_info.get('SeasonName')} {season_item_info.get('IndexNumber')} {season_item_info.get('Name')} 演员信息已更新") + retry = 3 + continue + season_item_info.update({ + "People": item_info.get("People") + }) + season_item_info["LockedFields"].append("Cast") + flag = self.__update_item_info(season_item.get("Id"), season_item_info) + logger.info( + f"更新媒体:{item.get('Name')} {season_item_info.get('SeasonName')} {season_item_info.get('IndexNumber')} {season_item_info.get('Name')} 成功:{flag}") + retry = 3 + time.sleep(0.5) + except Exception as e: + retry += 1 + logger.error( + f"更新媒体:{item.get('Name')} {season_item_info.get('SeasonName')} {season_item_info.get('IndexNumber')} {season_item_info.get('Name')} 信息出错:{e} 开始重试...{retry} / 3") + + logger.info(f"Emby剧集演员同步完成") + + def __update_item_info(self, item_id, data): + headers = { + 'accept': '*/*', + 'Content-Type': 'application/json' + } + res = RequestUtils(headers=headers).post( + f"{self._EMBY_HOST}/emby/Items/{item_id}?api_key={self._EMBY_APIKEY}", + data=json.dumps(data)) + if res and res.status_code == 204: + return True + return False + + def __get_items(self, parent_id) -> list: + """ + 获取媒体库媒体列表 + """ + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return [] + req_url = f"%semby/Users/%s/Items?ParentId=%s&api_key=%s" % ( + self._EMBY_HOST, self._EMBY_USER, parent_id, self._EMBY_APIKEY) + try: + with RequestUtils().get_res(req_url) as res: + if res: + return res.json().get("Items") + else: + logger.info(f"获取媒体库媒体列表失败,无法连接Emby!") + return [] + except Exception as e: + logger.error(f"连接媒体库媒体列表Items出错:" + str(e)) + return [] + + def __get_item_info(self, item_id): + res = RequestUtils().get_res( + f"{self._EMBY_HOST}/emby/Users/{self._EMBY_USER}/Items/{item_id}?api_key={self._EMBY_APIKEY}") + if res and res.status_code == 200: + return res.json() + return {} + + @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、数据结构 + """ + librarys = Emby().get_librarys() + library_options = [{'title': library.name, 'value': library.name} for library in librarys if + str(library.type) == "tvshows"] + return [ + { + "component": "VForm", + "content": [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + }, + ] + }, + { + "component": "VRow", + "content": [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'librarys', + 'label': '媒体库', + 'items': library_options + } + } + ] + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '可选同步媒体库,不选同步所有剧集媒体库。注:只支持Emby。' + } + } + ] + } + ] + } + ], + } + ], { + "onlyonce": False, + "librarys": [], + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + pass