From 90d18b7f8f47e8dedb9b9a3c8ce044efac9b77dd Mon Sep 17 00:00:00 2001 From: thsrite Date: Thu, 7 Nov 2024 15:39:12 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E4=BA=91=E7=9B=98Strm=E5=8A=A9=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/cloudcompanion.png | Bin 0 -> 3840 bytes package.v2.json | 12 + plugins.v2/cloudstrmcompanion/__init__.py | 808 ++++++++++++++++++ .../cloudstrmcompanion/requirements.txt | 1 + 4 files changed, 821 insertions(+) create mode 100644 icons/cloudcompanion.png create mode 100644 plugins.v2/cloudstrmcompanion/__init__.py create mode 100644 plugins.v2/cloudstrmcompanion/requirements.txt diff --git a/icons/cloudcompanion.png b/icons/cloudcompanion.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c6e3f101b40b98d2f782b41f878fe31b10635f GIT binary patch literal 3840 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

~aCog3$K! zPJZMWqE`fwXHq(KrZ|hX&j<|TK$VFxR6B;8&(8rx(_RL;^-azB)e(y!EU1>k)XcpL z!O*eS?wcPT%QLS-eXp5M?+*N$SpFr74bc)A*h(>h9+XSE;dFO#j4;~Sa0pUeERM*R z{baBdG2Xxrf8@iCQXjKeC(IQ{2Tw0ypA#&QO^1F z`PdM1`S$_=d+~8&W!KVS=*#}HsWU8<`M1MjVF+FdwhTCGDKl{OcMTd|PyMJQW7o_r z?X0AB9c%XSQHvm7tu0$_-9tdJbh0=3CYZRck70YM1WhM+FLsrYJDlY({V!fINtS-^ zO~yxCgK;n?6;`J_lW0*IfTwfCNUK7ey2`OBXNu&=g3u)u#A2R)v{A>DkS7PM zvnQZ>)QVu3TJD5Cbeh){sJ}JH1dKLl`7fBG{@HrXp*;{)EBJI&CoR|r>VHZr3f!wD zb&@uf1_1W}Oj^L+J~QeE{p%K}(IwZOdmAYwRDCydb?m}b#Nw6xNCmKl> zEgTkvV&jS*P?#sWOtERZ;ZRhIo#<0+#R_jPX=byOyQxSe-wRdVi&$*^xnxei1Hu&q zCk)aeM4@p;>N{c7HwsOY@_*mjzGQIP0*(9&EZ%zmlS$kuT7caXr7vFr8DWrm9P-}R z39RCtPqK4!mXJ#@uu9E26@^O^a<2>}(@Ie}bBM)|*}}V03bw>H&T9j&=$6&2|B%o~ z3$XoAvl7XB*#6>2Y$u5qd0`^*Wrcvo98cVTUkoC{Ey1T43x}Q^q=z!OaZ`(o;M>~5 znh1RfUq9iv%XDSbj*>*gYB81~LagHVxghTDwExlq9%k(?y7<&t{fYGp*&04JsU{V& zwBy5+gbaxlYeG?+dqOL}I5P^h_Lxs=mw>H&G=t}VJ5AfLC|8l-WZ44j&AeQRD?>_% zL{+_V>?_jy0J#iCF&+mYo*s{)=BVT;FQle`ThjKoP(n}x;D-m?2YK`+E) z%!H;D+3k$rs`@Acc_B9l2Va>LTmu-EAO;J*JxN|~y%IA7iU2^ih|3iE|42}e!I8eh V`&9q>#FI!1+_+|}TdiXk^B {strm_content}") + except Exception as e: + logger.error(f"创建strm文件失败 {strm_file} -> {str(e)}") + + def export_dir(self, fid, destination_id="0"): + """ + 获取目录导出id + """ + export_api = "https://webapi.115.com/files/export_dir" + response = requests.post(url=export_api, + headers=self._headers, + data={"file_ids": fid, "target": f"U_1_{destination_id}"}) + if response.status_code == 200: + result = response.json() + if result.get("state"): + export_id = result.get("data", {}).get("export_id") + + retry_cnt = 60 + while retry_cnt > 0: + response = requests.get(url=export_api, + headers=self._headers, + data={"export_id": export_id}) + if response.status_code == 200: + result = response.json() + if result.get("state"): + if str(export_id) == str(result.get("data", {}).get("export_id")): + return result.get("data", {}).get("pick_code"), result.get("data", {}).get("file_id") + retry_cnt -= 1 + logger.info(f"等待目录树生成完成,剩余重试 {retry_cnt} 次") + time.sleep(3) + return None + + def retrieve_directory_structure(self, directory_path): + """ + 获取目录树结构 + """ + file_id = None + try: + logger.info(f"开始生成 {directory_path} 目录树") + dir_info = self._115client.fs_dir_getid(directory_path) + if not dir_info: + logger.error(f"{directory_path} 目录不存在或路径错误") + return + fid = dir_info.get("id") + if not fid: + logger.error(f"{directory_path} 目录不存在或路径错误") + return + pick_code, file_id = self.export_dir(fid) + if not pick_code: + logger.error(f"{directory_path} 生成目录数失败") + return + # 获取目录树下载链接 + download_url = self._115client.download_url(pick_code, headers=self._headers) + directory_content = self.fetch_content(download_url) + logger.info(f"{directory_path} 目录树下载成功") + return directory_content + except Exception as e: + logger.error(f"{directory_path} 目录树生成失败: {str(e)}") + finally: + if file_id: + try: + self._115client.fs_delete(file_id) + except: + pass + + def fetch_content(self, url): + """ + 下载目录树文件内容 + """ + try: + with requests.get(url, headers=self._headers, stream=True, timeout=60) as response: + response.raise_for_status() + content = BytesIO() + for chunk in response.iter_content(chunk_size=8192): + content.write(chunk) + return content.getvalue().decode("utf-16") + except: + logger.error(f"文件下载失败: {traceback.format_exc()}") + return None + + @staticmethod + def parse_tree_structure(content: str): + """ + 解析目录树内容并生成每个路径 + """ + tree_pattern = re_compile(r"^(?:\| )+\|-") + current_path = ["/"] # 初始化当前路径为根目录 + + for line in content.splitlines(): + # 匹配目录树的每一行 + match = tree_pattern.match(line) + if not match or "根目录" in line: + continue # 跳过不符合格式的行 + + # 计算当前行的深度 + level_indicator = match.group(0) + depth = (len(level_indicator) // 2) - 1 + # 获取当前行的目录名称 + item_name = escape(line.strip()[len(level_indicator):]) + + # 根据深度更新当前路径 + if depth < len(current_path): + current_path[depth] = item_name # 更新已有深度的名称 + else: + current_path.append(item_name) # 添加新的深度名称 + + # 生成并返回当前深度的完整路径 + yield join_path(*current_path[:depth + 1]) + + def __update_config(self): + """ + 更新配置 + """ + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "cover": self._cover, + "rebuild": self._rebuild, + "monitor": self._monitor, + "cron": self._cron, + "monitor_confs": self._monitor_confs, + "115_cookie": self._115_cookie, + }) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + return [{ + "cmd": "/CloudStrmCompanion", + "event": EventType.PluginAction, + "desc": "云盘Strm助手同步", + "category": "", + "data": { + "action": "CloudStrmCompanion" + } + }] + + def get_service(self) -> List[Dict[str, Any]]: + """ + 注册插件公共服务 + [{ + "id": "服务ID", + "name": "服务名称", + "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", + "func": self.xxx, + "kwargs": {} # 定时器参数 + }] + """ + if self._enabled and self._cron: + return [{ + "id": "CloudStrmCompanion", + "name": "云盘Strm助手同步", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.scan, + "kwargs": {} + }] + return [] + + 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': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'monitor', + 'label': '实时监控', + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '全量同步一次', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'rebuild', + 'label': '重建缓存', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'cover', + 'label': '覆盖已存在文件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '同步周期', + 'placeholder': '0 0 * * *' + } + } + ] + }, + + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': '115_cookie', + 'label': '115Cookie', + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'monitor_confs', + 'label': '目录配置', + 'rows': 5, + 'placeholder': 'MoviePilot中云盘挂载本地的路径#MoviePilot中strm生成路径#alist/cd2上115路径#strm格式化' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': 'MoviePilot中云盘挂载本地的路径:/mnt/media/series/国产剧/雪迷宫 (2024);' + 'MoviePilot中strm生成路径:/mnt/library/series/国产剧/雪迷宫 (2024);' + '云盘路径:/cloud/media/series/国产剧/雪迷宫 (2024);' + '则目录配置为:/mnt/media#/mnt/library#/cloud/media#{local_file}' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': 'strm格式化方式:' + '1.本地源文件路径:{local_file}。' + '2.alist路径:http://192.168.31.103:5244/d{cloud_file}。' + '3.cd2路径:http://192.168.31.103:19798/static/http/192.168.31.103:19798/False/{cloud_file}。' + '4.其他api路径:http://192.168.31.103:2001/{cloud_file}' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "cron": "", + "onlyonce": False, + "rebuild": False, + "monitor": False, + "cover": False, + "monitor_confs": "", + "115_cookie": "", + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + if self._observer: + for observer in self._observer: + try: + observer.stop() + observer.join() + except Exception as e: + print(str(e)) + self._observer = [] + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._event.set() + self._scheduler.shutdown() + self._event.clear() + self._scheduler = None diff --git a/plugins.v2/cloudstrmcompanion/requirements.txt b/plugins.v2/cloudstrmcompanion/requirements.txt new file mode 100644 index 0000000..0d563fb --- /dev/null +++ b/plugins.v2/cloudstrmcompanion/requirements.txt @@ -0,0 +1 @@ +python-115 \ No newline at end of file