diff --git a/README.md b/README.md index 24a0f82..88fcd56 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,4 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - 实时软连接 1.3 - 订阅规则自动填充 2.5 - Emby元数据刷新 1.0 +- Emby媒体标签 1.0 diff --git a/icons/tag.png b/icons/tag.png new file mode 100644 index 0000000..fd73431 Binary files /dev/null and b/icons/tag.png differ diff --git a/package.json b/package.json index f3f0985..991c50f 100644 --- a/package.json +++ b/package.json @@ -388,5 +388,17 @@ "history": { "v1.0": "定时刷新Emby媒体库元数据" } + }, + "EmbyMetaTag": { + "name": "Emby媒体标签", + "description": "自动给媒体库媒体添加标签。", + "labels": "Emby", + "version": "1.0", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/tag.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.0": "自动给媒体库媒体添加标签" + } } } diff --git a/plugins/embymetarefresh/__init__.py b/plugins/embymetarefresh/__init__.py index a281857..ef08a3f 100644 --- a/plugins/embymetarefresh/__init__.py +++ b/plugins/embymetarefresh/__init__.py @@ -37,6 +37,7 @@ class EmbyMetaRefresh(_PluginBase): _onlyonce = False _cron = None _days = None + _emby = None _EMBY_HOST = settings.EMBY_HOST _EMBY_APIKEY = settings.EMBY_API_KEY _scheduler: Optional[BackgroundScheduler] = None @@ -44,6 +45,7 @@ class EmbyMetaRefresh(_PluginBase): def init_plugin(self, config: dict = None): # 停止现有任务 self.stop_service() + self._emby = Emby() if config: self._enabled = config.get("enabled") @@ -133,7 +135,7 @@ class EmbyMetaRefresh(_PluginBase): 刷新emby """ if transferinfo.type == "电影": - movies = Emby().get_movies(title=transferinfo.title, year=transferinfo.year) + movies = self._emby.get_movies(title=transferinfo.title, year=transferinfo.year) if not movies: logger.error(f"Emby中没有找到{transferinfo.title} ({transferinfo.year})") return @@ -147,7 +149,7 @@ class EmbyMetaRefresh(_PluginBase): return # 验证tmdbid是否相同 - item_info = Emby().get_iteminfo(item_id) + item_info = self._emby.get_iteminfo(item_id) if item_info: if transferinfo.tmdbid and item_info.tmdbid: if str(transferinfo.tmdbid) != str(item_info.tmdbid): diff --git a/plugins/embymetatag/__init__.py b/plugins/embymetatag/__init__.py new file mode 100644 index 0000000..9c7a6fb --- /dev/null +++ b/plugins/embymetatag/__init__.py @@ -0,0 +1,336 @@ +from datetime import datetime, timedelta +from typing import Optional, Any, List, Dict, Tuple + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +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 EmbyMetaTag(_PluginBase): + # 插件名称 + plugin_name = "Emby媒体标签" + # 插件描述 + plugin_desc = "自动给媒体库媒体添加标签。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/tag.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "embymetatag_" + # 加载顺序 + plugin_order = 16 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _enabled = False + _onlyonce = False + _cron = None + _tag_confs = None + _emby = None + _EMBY_HOST = settings.EMBY_HOST + _EMBY_APIKEY = settings.EMBY_API_KEY + _EMBY_USER = Emby().get_user() + _scheduler: Optional[BackgroundScheduler] = None + + _tags = {} + + def init_plugin(self, config: dict = None): + # 停止现有任务 + self.stop_service() + _emby = Emby() + + if config: + self._enabled = config.get("enabled") + self._onlyonce = config.get("onlyonce") + self._cron = config.get("cron") + self._tag_confs = config.get("tag_confs") + + 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._tag_confs: + tag_confs = self._tag_confs.split("\n") + for tag_conf in tag_confs: + if tag_conf: + tag_conf = tag_conf.split("#") + if len(tag_conf) == 2: + librarys = tag_conf[0].split(',') + for library in librarys: + self._tags[library] = tag_conf[1].split(',') + + # 加载模块 + if self._enabled or self._onlyonce: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + # 立即运行一次 + if self._onlyonce: + logger.info(f"Emby媒体标签服务启动,立即运行一次") + self._scheduler.add_job(self.auto_tag, 'date', + run_date=datetime.now( + tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="Emby媒体标签") + + # 关闭一次性开关 + self._onlyonce = False + + # 保存配置 + self.__update_config() + + # 周期运行 + if self._cron: + try: + self._scheduler.add_job(func=self.auto_tag, + trigger=CronTrigger.from_crontab(self._cron), + name="Emby媒体标签") + except Exception as err: + logger.error(f"定时任务配置错误:{str(err)}") + # 推送实时消息 + self.systemmessage.put(f"执行周期配置错误:{err}") + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def get_state(self) -> bool: + return self._enabled + + def __update_config(self): + self.update_config( + { + "onlyonce": self._onlyonce, + "cron": self._cron, + "enabled": self._enabled, + "tag_confs": self._tag_confs, + } + ) + + def auto_tag(self): + """ + 给设定媒体库打标签 + """ + if "emby" not in settings.MEDIASERVER: + logger.error("未配置Emby媒体服务器") + return + + if not self._tags or len(self._tags.keys()) == 0: + logger.error("未配置Emby媒体标签") + return + + # 获取emby 媒体库 + librarys = self._emby.get_librarys() + if not librarys: + logger.error("获取媒体库失败") + return + + # 遍历媒体库,获取媒体库媒体 + for library in librarys: + library_tags = self._tags.get(library.name) + if not library_tags: + continue + + # 获取媒体库媒体 + library_items = self._emby.get_items(library.id) + if not library_items: + continue + + for library_item in library_items: + # 获取item的tag + item_tags = self.__get_item_tags(library_item.id) or [] + + # 获取缺少的tag + add_tags = [] + for library_tag in library_tags: + if not item_tags or library_tag not in item_tags: + add_tags.append(library_tag) + + # 添加标签 + if add_tags: + tags = [{'Name': add_tag} for add_tag in add_tags] + self.__add_tag(library_item.id, {"Tags": tags}) + logger.info(f"添加标签成功:{library.name} {library_item.name} {add_tags}") + + def __add_tag(self, itemid: str, tags: dict): + req_url = "%semby/Items/%s/Tags/Add?api_key=%s" % (self._EMBY_HOST, self._EMBY_USER, itemid, self._EMBY_APIKEY) + try: + with RequestUtils().post_res(url=req_url, json=tags) as res: + if res and res.status_code == 204: + return True + except Exception as e: + logger.error(f"连接Items/Id/Tags/Add出错:" + str(e)) + return False + + def __get_item_tags(self, itemid: str): + """ + 获取单个项目详情 + """ + if not itemid: + return None + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return None + req_url = "%semby/Users/%s/Items/%s?api_key=%s" % (self._EMBY_HOST, self._EMBY_USER, itemid, self._EMBY_APIKEY) + try: + with RequestUtils().get_res(req_url) as res: + if res and res.status_code == 200: + item = res.json() + return [tag.get('Name') for tag in item.get("TagItems")] + except Exception as e: + logger.error(f"连接Items/Id出错:" + str(e)) + 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、数据结构 + """ + 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': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + }, + ] + }, + { + "component": "VRow", + "content": [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动' + } + } + ] + } + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'tag_confs', + 'label': '标签配置', + 'rows': 3, + 'placeholder': '媒体库名,媒体库名#标签名,标签名' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '定时刷新Emby媒体库媒体,添加自定义标签。' + } + } + ] + } + ] + } + ], + } + ], { + "enabled": False, + "onlyonce": False, + "cron": "5 1 * * *", + "tag_confs": "", + } + + def get_page(self) -> List[dict]: + pass + + 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))