From 5788f87359ec879fc3185962fbcbe0c237d7c419 Mon Sep 17 00:00:00 2001 From: thsrite Date: Sat, 4 May 2024 21:06:38 +0800 Subject: [PATCH] =?UTF-8?q?feat=20Emby=E5=AA=92=E4=BD=93=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + icons/tag.png | Bin 0 -> 5276 bytes package.json | 12 + plugins/embymetarefresh/__init__.py | 6 +- plugins/embymetatag/__init__.py | 336 ++++++++++++++++++++++++++++ 5 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 icons/tag.png create mode 100644 plugins/embymetatag/__init__.py 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 0000000000000000000000000000000000000000..fd734310cd6ef300d1623b58bd02738d27f29341 GIT binary patch literal 5276 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 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))