From 6f9e7137666becf380e0947010b8711fa4cbc012 Mon Sep 17 00:00:00 2001 From: thsrite Date: Mon, 26 Aug 2024 11:34:03 +0800 Subject: [PATCH] =?UTF-8?q?feat=20Emby=E5=BC=B9=E5=B9=95=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + icons/danmu.png | Bin 0 -> 4168 bytes package.json | 12 + plugins/embydanmu/__init__.py | 514 ++++++++++++++++++++++++++++++++++ 4 files changed, 527 insertions(+) create mode 100644 icons/danmu.png create mode 100644 plugins/embydanmu/__init__.py diff --git a/README.md b/README.md index 0e68c48..dfa8983 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,4 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - 影视将映订阅 v1.1 - Emby视频类型检查 v1.0 - Emby有声书整理 v1.1 +- Emby弹幕下载 v1.0 \ No newline at end of file diff --git a/icons/danmu.png b/icons/danmu.png new file mode 100644 index 0000000000000000000000000000000000000000..6c191182b04f9e3520c631ca3bf4abdb99701d42 GIT binary patch literal 4168 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 S7x?G^0000 0: + retry_cnt -= 1 + logger.warn( + f"{parent_path} 下未找到字幕文件:{danmu_path_pattern},等待60秒后重试 ({retry_cnt}次)") + time.sleep(60) + + if len(list(parent_path.glob(danmu_path_pattern))) >= 1: + logger.info(f"{parent_path} 下已找到字幕文件:{danmu_path_pattern}") + self.post_message(channel=event.event_data.get("channel"), + title=f"{library_name} {library_item_name} 下载字幕文件成功", + userid=event.event_data.get("user")) + else: + logger.error(f"{parent_path} 下未找到字幕文件:{danmu_path_pattern}") + self.post_message(channel=event.event_data.get("channel"), + title=f"{library_name} {library_item_name} 下载字幕文件失败", + userid=event.event_data.get("user")) + else: + logger.error( + f"通知弹幕插件获取 {library_name} 电影 {library_item_name} {movie_id} 的弹幕失败") + self.post_message(channel=event.event_data.get("channel"), + title=f"通知弹幕插件获取 {library_name} 电影 {library_item_name} {movie_id} 的弹幕失败", + userid=event.event_data.get("user")) + + # 关闭弹幕插件 + logger.info(f"获取弹幕任务完成,关闭弹幕插件") + # 禁用媒体库的Danmu插件 + library_disabled_subtitle_fetchers = library_options.get("DisabledSubtitleFetchers", []) + library_disabled_subtitle_fetchers.append("Danmu") + library_options.update({ + "DisabledSubtitleFetchers": library_disabled_subtitle_fetchers, + }) + update_flag = self.__update_library(library_id, library_options) + if update_flag: + logger.info(f"已禁用媒体库:{library_name}的Danmu插件") + else: + logger.error(f"禁用媒体库:{library_name}的Danmu插件失败") + + def get_state(self) -> bool: + return self._enabled + + def __get_librarys(self) -> list: + """ + 获取媒体库信息 + """ + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return [] + req_url = f"%semby/Library/VirtualFolders/Query?api_key=%s" % ( + self._EMBY_HOST, 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"连接媒体库emby/Library/VirtualFolders/Query出错:" + str(e)) + return [] + + def __update_library(self, library_id, library_options) -> bool: + """ + 获取媒体库信息 + """ + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return False + headers = { + 'accept': '*/*', + 'Content-Type': 'application/json' + } + req_url = f"%semby/Library/VirtualFolders/LibraryOptions?api_key=%s" % ( + self._EMBY_HOST, self._EMBY_APIKEY) + res = RequestUtils(headers=headers).post(url=req_url, + data=json.dumps({"Id": library_id, "LibraryOptions": library_options})) + 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 __download_danmu(self, item_id) -> bool: + """ + 通知Danmu插件获取弹幕 + """ + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return [] + req_url = f"%sapi/danmu/%s?option=Refresh&api_key=%s" % ( + self._EMBY_HOST, item_id, self._EMBY_APIKEY) + try: + with RequestUtils().get_res(req_url) as res: + if res: + return res.text == "ok" + else: + logger.info(f"通知Danmu插件获取弹幕失败,无法连接Emby!") + return False + except Exception as e: + logger.error(f"通知Danmu插件获取弹幕api/danmu/{item_id}?option=Refresh出错:" + str(e)) + return False + + def __get_item_info(self, item_id) -> dict: + """ + 获取媒体详情 + """ + if not self._EMBY_HOST or not self._EMBY_APIKEY: + return {} + req_url = f"%semby/Users/%s/Items/%s?fields=ShareLevel&ExcludeFields=Chapters,Overview,People,MediaStreams,Subviews&api_key=%s" % ( + self._EMBY_HOST, self._EMBY_USER, item_id, self._EMBY_APIKEY) + with RequestUtils().get_res(req_url) as res: + if res: + return res.json() + else: + logger.info(f"获取媒体详情失败,无法连接Emby!") + return {} + + def __check_danmu_exists(self, season_id): + """ + 检查媒体是否有弹幕 + """ + season_items = self.__get_items(season_id) + item_path = season_items[0].get("Path") + parent_path = Path(item_path).parent + logger.info(f"开始检查路径 {parent_path} 下是是否有字幕文件") + # 检查是否有字幕文件 + danmu_path_pattern = "*.xml" + + retry_cnt = len(season_items) + _downloaded_danmu_files = [] + while len(_downloaded_danmu_files) < len(season_items) and retry_cnt > 0: + danmu_files = list(parent_path.glob(danmu_path_pattern)) + for danmu_file in danmu_files: + if danmu_file.name not in _downloaded_danmu_files: + _downloaded_danmu_files.append(danmu_file.name) + logger.info(f"已下载字幕文件:{danmu_file.name}") + retry_cnt -= 1 + logger.warn( + f"{parent_path} 下字幕文件:{danmu_path_pattern} 未下载完成,等待60秒后重试 ({retry_cnt}次)") + time.sleep(60) + + return len(_downloaded_danmu_files), len(season_items) + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + return [ + { + "cmd": "/danmu", + "event": EventType.PluginAction, + "desc": "emby弹幕下载", + "category": "", + "data": { + "action": "embydanmu" + } + } + ] + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_service(self) -> List[Dict[str, Any]]: + pass + + 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': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '仅支持交互命令运行: /danmu 媒体库名 媒体名 (季)。 季可选,不填则获取全部季度。' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '需Emby安装Danmu插件,并启用弹幕功能(https://github.com/fengymi/emby-plugin-danmu)。' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + pass