diff --git a/quark_auto_save.py b/quark_auto_save.py index 4ae26ca..cf11b8e 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -1,6 +1,6 @@ # !/usr/bin/env python3 # -*- coding: utf-8 -*- -# Modify: 2024-11-13 +# Modify: 2024-04-03 # Repo: https://github.com/Cp0204/quark_auto_save # ConfigFile: quark_config.json """ @@ -14,9 +14,10 @@ import json import time import random import requests -import importlib from datetime import datetime +MAX_SAVE_FILES = 2 # 最大保存文件数量 + # 兼容青龙 try: from treelib import Tree @@ -33,12 +34,22 @@ GH_PROXY = os.environ.get("GH_PROXY", "https://ghproxy.net/") MAGIC_REGEX = { "$TV": { - "pattern": r".*?(?= response["metadata"]["_total"]: + if len(file_list) >= response["metadata"]["_total"]: break - response["data"]["list"] = list_merge - return response["data"] + return file_list def get_fids(self, file_paths): fids = [] while True: - url = f"{self.BASE_URL}/1/clouddrive/file/info/path_list" + url = "https://drive-m.quark.cn/1/clouddrive/file/info/path_list" querystring = {"pr": "ucpro", "fr": "pc"} payload = {"file_path": file_paths[:50], "namespace": "0"} - response = self._send_request( - "POST", url, json=payload, params=querystring + headers = self.common_headers() + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring ).json() if response["code"] == 0: fids += response["data"] @@ -350,11 +287,11 @@ class Quark: break return fids - def ls_dir(self, pdir_fid, **kwargs): + def ls_dir(self, pdir_fid): file_list = [] page = 1 while True: - url = f"{self.BASE_URL}/1/clouddrive/file/sort" + url = "https://drive-m.quark.cn/1/clouddrive/file/sort" querystring = { "pr": "ucpro", "fr": "pc", @@ -365,9 +302,11 @@ class Quark: "_fetch_total": "1", "_fetch_sub_dirs": "0", "_sort": "file_type:asc,updated_at:desc", - "_fetch_full_path": kwargs.get("fetch_full_path", 0), } - response = self._send_request("GET", url, params=querystring).json() + headers = self.common_headers() + response = requests.request( + "GET", url, headers=headers, params=querystring + ).json() if response["data"]["list"]: file_list += response["data"]["list"] page += 1 @@ -378,7 +317,7 @@ class Quark: return file_list def save_file(self, fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken): - url = f"{self.BASE_URL}/1/clouddrive/share/sharepage/save" + url = "https://drive-m.quark.cn/1/clouddrive/share/sharepage/save" querystring = { "pr": "ucpro", "fr": "pc", @@ -387,62 +326,40 @@ class Quark: "__dt": int(random.uniform(1, 5) * 60 * 1000), "__t": datetime.now().timestamp(), } + querystring["fr"] = "h5" if self.st else "pc" + + # 限制最多保存2个文件 + files_to_save = fid_list[:MAX_SAVE_FILES] + tokens_to_save = fid_token_list[:MAX_SAVE_FILES] + payload = { - "fid_list": fid_list, - "fid_token_list": fid_token_list, + "fid_list": files_to_save, + "fid_token_list": tokens_to_save, "to_pdir_fid": to_pdir_fid, "pwd_id": pwd_id, "stoken": stoken, "pdir_fid": "0", "scene": "link", } - response = self._send_request( - "POST", url, json=payload, params=querystring - ).json() - return response + headers = self.common_headers() + + try: + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring + ).json() + except requests.RequestException as e: + print(f"请求失败: {e}") + return {"code": -1, "message": "请求失败"} + + return { + "code": response.get("code", -1), # 确保返回包含 'code' + "message": response.get("message", ""), + "data": response.get("data", {}) + } - def query_task(self, task_id): - retry_index = 0 - while True: - url = f"{self.BASE_URL}/1/clouddrive/task" - querystring = { - "pr": "ucpro", - "fr": "pc", - "uc_param_str": "", - "task_id": task_id, - "retry_index": retry_index, - "__dt": int(random.uniform(1, 5) * 60 * 1000), - "__t": datetime.now().timestamp(), - } - response = self._send_request("GET", url, params=querystring).json() - if response["data"]["status"] != 0: - if retry_index > 0: - print() - break - else: - if retry_index == 0: - print( - f"正在等待[{response['data']['task_title']}]执行结果", - end="", - flush=True, - ) - else: - print(".", end="", flush=True) - retry_index += 1 - time.sleep(0.500) - return response - - def download(self, fids): - url = f"{self.BASE_URL}/1/clouddrive/file/download" - querystring = {"pr": "ucpro", "fr": "pc", "uc_param_str": ""} - payload = {"fids": fids} - response = self._send_request("POST", url, json=payload, params=querystring) - set_cookie = response.cookies.get_dict() - cookie_str = "; ".join([f"{key}={value}" for key, value in set_cookie.items()]) - return response.json(), cookie_str def mkdir(self, dir_path): - url = f"{self.BASE_URL}/1/clouddrive/file" + url = "https://drive-m.quark.cn/1/clouddrive/file" querystring = {"pr": "ucpro", "fr": "pc", "uc_param_str": ""} payload = { "pdir_fid": "0", @@ -450,31 +367,34 @@ class Quark: "dir_path": dir_path, "dir_init_lock": False, } - response = self._send_request( - "POST", url, json=payload, params=querystring + headers = self.common_headers() + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring ).json() return response def rename(self, fid, file_name): - url = f"{self.BASE_URL}/1/clouddrive/file/rename" + url = "https://drive-m.quark.cn/1/clouddrive/file/rename" querystring = {"pr": "ucpro", "fr": "pc", "uc_param_str": ""} payload = {"fid": fid, "file_name": file_name} - response = self._send_request( - "POST", url, json=payload, params=querystring + headers = self.common_headers() + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring ).json() return response def delete(self, filelist): - url = f"{self.BASE_URL}/1/clouddrive/file/delete" + url = "https://drive-m.quark.cn/1/clouddrive/file/delete" querystring = {"pr": "ucpro", "fr": "pc", "uc_param_str": ""} payload = {"action_type": 2, "filelist": filelist, "exclude_fids": []} - response = self._send_request( - "POST", url, json=payload, params=querystring + headers = self.common_headers() + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring ).json() return response def recycle_list(self, page=1, size=30): - url = f"{self.BASE_URL}/1/clouddrive/file/recycle/list" + url = "https://drive-m.quark.cn/1/clouddrive/file/recycle/list" querystring = { "_page": page, "_size": size, @@ -482,48 +402,25 @@ class Quark: "fr": "pc", "uc_param_str": "", } - response = self._send_request("GET", url, params=querystring).json() + headers = self.common_headers() + response = requests.request( + "GET", url, headers=headers, params=querystring + ).json() return response["data"]["list"] def recycle_remove(self, record_list): - url = f"{self.BASE_URL}/1/clouddrive/file/recycle/remove" + url = "https://drive-m.quark.cn/1/clouddrive/file/recycle/remove" querystring = {"uc_param_str": "", "fr": "pc", "pr": "ucpro"} payload = { "select_mode": 2, "record_list": record_list, } - response = self._send_request( - "POST", url, json=payload, params=querystring + headers = self.common_headers() + response = requests.request( + "POST", url, json=payload, headers=headers, params=querystring ).json() return response - # ↑ 请求函数 - # ↓ 操作函数 - - # 魔法正则匹配 - def magic_regex_func(self, pattern, replace, taskname=None): - magic_regex = CONFIG_DATA.get("magic_regex") or MAGIC_REGEX or {} - keyword = pattern - if keyword in magic_regex: - pattern = magic_regex[keyword]["pattern"] - if replace == "": - replace = magic_regex[keyword]["replace"] - if taskname: - replace = replace.replace("$TASKNAME", taskname) - return pattern, replace - - def get_id_from_url(self, url): - url = url.replace("https://pan.quark.cn/s/", "") - pattern = r"(\w+)(\?pwd=(\w+))?(#/list/share.*/(\w+))?" - match = re.search(pattern, url) - if match: - pwd_id = match.group(1) - passcode = match.group(3) if match.group(3) else "" - pdir_fid = match.group(5) if match.group(5) else 0 - return pwd_id, passcode, pdir_fid - else: - return None - def update_savepath_fid(self, tasklist): dir_paths = [ re.sub(r"/{2,}", "/", f"/{item['savepath']}") @@ -557,21 +454,36 @@ class Quark: def do_save_check(self, shareurl, savepath): try: - pwd_id, passcode, pdir_fid = self.get_id_from_url(shareurl) - is_sharing, stoken = self.get_stoken(pwd_id, passcode) - share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["list"] + pwd_id, pdir_fid = self.get_id_from_url(shareurl) + is_sharing, stoken = self.get_stoken(pwd_id) + share_file_list = self.get_detail(pwd_id, stoken, pdir_fid) fid_list = [item["fid"] for item in share_file_list] fid_token_list = [item["share_fid_token"] for item in share_file_list] file_name_list = [item["file_name"] for item in share_file_list] + if not fid_list: return + + # 获取目标目录的fid get_fids = self.get_fids([savepath]) to_pdir_fid = ( get_fids[0]["fid"] if get_fids else self.mkdir(savepath)["data"]["fid"] ) + + # 限制转存的文件总数为2个 + files_to_save = fid_list[:MAX_SAVE_FILES] + tokens_to_save = fid_token_list[:MAX_SAVE_FILES] + names_to_save = file_name_list[:MAX_SAVE_FILES] + + print(f"转存文件ID列表: {files_to_save}") + print(f"转存文件Token列表: {tokens_to_save}") + print(f"转存文件名称列表: {names_to_save}") + + # 保存文件 save_file = self.save_file( - fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken + files_to_save, tokens_to_save, to_pdir_fid, pwd_id, stoken ) + if save_file["code"] == 41017: return elif save_file["code"] == 0: @@ -579,24 +491,19 @@ class Quark: del_list = [ item["fid"] for item in dir_file_list - if (item["file_name"] in file_name_list) + if (item["file_name"] in names_to_save) and ((datetime.now().timestamp() - item["created_at"]) < 60) ] if del_list: - self.delete(del_list) - recycle_list = self.recycle_list() - record_id_list = [ - item["record_id"] - for item in recycle_list - if item["fid"] in del_list - ] - self.recycle_remove(record_id_list) - return save_file + for fid in del_list: + self.del_file(fid, pwd_id, stoken) + return "转存成功" else: - return False + return f"转存失败: {save_file['message']}" except Exception as e: - if os.environ.get("DEBUG") == True: - print(f"转存测试失败: {str(e)}") + return f"转存过程中出现错误: {str(e)}" + + def do_save_task(self, task): # 判断资源失效记录 @@ -605,35 +512,34 @@ class Quark: return # 链接转换所需参数 - pwd_id, passcode, pdir_fid = self.get_id_from_url(task["shareurl"]) - # print("match: ", pwd_id, pdir_fid) + pwd_id, pdir_fid = self.get_id_from_url(task["shareurl"]) # 获取stoken,同时可验证资源是否失效 - is_sharing, stoken = self.get_stoken(pwd_id, passcode) + is_sharing, stoken = self.get_stoken(pwd_id) if not is_sharing: add_notify(f"❌《{task['taskname']}》:{stoken}\n") task["shareurl_ban"] = stoken return - # print("stoken: ", stoken) updated_tree = self.dir_check_and_save(task, pwd_id, stoken, pdir_fid) if updated_tree.size(1) > 0: add_notify(f"✅《{task['taskname']}》添加追更:\n{updated_tree}") - return updated_tree + return True else: print(f"任务结束:没有新的转存任务") return False + def dir_check_and_save(self, task, pwd_id, stoken, pdir_fid="", subdir_path=""): tree = Tree() + tree.create_node(task["savepath"], pdir_fid) # 获取分享文件列表 - share_file_list = self.get_detail(pwd_id, stoken, pdir_fid)["list"] - # print("share_file_list: ", share_file_list) - + share_file_list = self.get_detail(pwd_id, stoken, pdir_fid) + if not share_file_list: if subdir_path == "": task["shareurl_ban"] = "分享为空,文件已被分享者删除" - add_notify(f"❌《{task['taskname']}》:{task['shareurl_ban']}\n") + add_notify(f"《{task['taskname']}》:{task['shareurl_ban']}") return tree elif ( len(share_file_list) == 1 @@ -641,10 +547,8 @@ class Quark: and subdir_path == "" ): # 仅有一个文件夹 print("🧠 该分享是一个文件夹,读取文件夹内列表") - share_file_list = self.get_detail( - pwd_id, stoken, share_file_list[0]["fid"] - )["list"] - + share_file_list = self.get_detail(pwd_id, stoken, share_file_list[0]["fid"]) + # 获取目标目录文件列表 savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}") if not self.savepath_fid.get(savepath): @@ -655,26 +559,17 @@ class Quark: return tree to_pdir_fid = self.savepath_fid[savepath] dir_file_list = self.ls_dir(to_pdir_fid) - # print("dir_file_list: ", dir_file_list) - - tree.create_node( - savepath, - pdir_fid, - data={ - "is_dir": True, - }, - ) - + # 需保存的文件清单 need_save_list = [] + # 添加符合的 for share_file in share_file_list: if share_file["dir"] and task.get("update_subdir", False): pattern, replace = task["update_subdir"], "" else: - pattern, replace = self.magic_regex_func( - task["pattern"], task["replace"], task["taskname"] - ) + pattern, replace = magic_regex_func(task["pattern"], task["replace"]) + # 正则文件名匹配 if re.search(pattern, share_file["file_name"]): # 替换后的文件名 @@ -691,6 +586,7 @@ class Quark: ) else: compare_func = lambda a, b1, b2: (a == b1 or a == b2) + # 判断目标目录文件是否存在 file_exists = any( compare_func( @@ -719,42 +615,34 @@ class Quark: "📁" + share_file["file_name"], share_file["fid"], parent=pdir_fid, - data={ - "is_dir": share_file["dir"], - }, ) tree.merge(share_file["fid"], subdir_tree, deep=False) - # 指定文件开始订阅/到达指定文件(含)结束历遍 - if share_file["fid"] == task.get("startfid", ""): - break + # 限制最多保存2个文件 + need_save_list = need_save_list[:MAX_SAVE_FILES] fid_list = [item["fid"] for item in need_save_list] fid_token_list = [item["share_fid_token"] for item in need_save_list] + save_name_list = [item["save_name"] for item in need_save_list] + if fid_list: save_file_return = self.save_file( fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken ) err_msg = None if save_file_return["code"] == 0: - task_id = save_file_return["data"]["task_id"] + task_id = save_file_return["data"].get("task_id", "") query_task_return = self.query_task(task_id) if query_task_return["code"] == 0: + save_name_list.sort() # 建立目录树 - for index, item in enumerate(need_save_list): + for item in need_save_list: icon = ( "📁" if item["dir"] == True else "🎞️" if item["obj_category"] == "video" else "" ) tree.create_node( - f"{icon}{item['save_name']}", - item["fid"], - parent=pdir_fid, - data={ - "fid": f"{query_task_return['data']['save_as']['save_as_top_fids'][index]}", - "path": f"{savepath}/{item['save_name']}", - "is_dir": item["dir"], - }, + f"{icon}{item['save_name']}", item["fid"], parent=pdir_fid ) else: err_msg = query_task_return["message"] @@ -762,13 +650,52 @@ class Quark: err_msg = save_file_return["message"] if err_msg: add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n") + + # 打印实际保存的文件名 + if save_name_list: + print(f"实际保存的文件名: {', '.join(save_name_list)}") + return tree + + + + def query_task(self, task_id): + retry_index = 0 + while True: + url = "https://drive-m.quark.cn/1/clouddrive/task" + querystring = { + "pr": "ucpro", + "fr": "pc", + "uc_param_str": "", + "task_id": task_id, + "retry_index": retry_index, + "__dt": int(random.uniform(1, 5) * 60 * 1000), + "__t": datetime.now().timestamp(), + } + headers = self.common_headers() + response = requests.request( + "GET", url, headers=headers, params=querystring + ).json() + if response["data"]["status"] != 0: + if retry_index > 0: + print() + break + else: + if retry_index == 0: + print( + f"正在等待[{response['data']['task_title']}]执行结果", + end="", + flush=True, + ) + else: + print(".", end="", flush=True) + retry_index += 1 + time.sleep(0.500) + return response + def do_rename_task(self, task, subdir_path=""): - pattern, replace = self.magic_regex_func( - task["pattern"], task["replace"], task["taskname"] - ) - if not pattern or not replace: + if not task["pattern"] or not task["replace"]: return 0 savepath = re.sub(r"/{2,}", "/", f"/{task['savepath']}{subdir_path}") if not self.savepath_fid.get(savepath): @@ -781,6 +708,7 @@ class Quark: is_rename_count += self.do_rename_task( task, f"{subdir_path}/{dir_file['file_name']}" ) + pattern, replace = magic_regex_func(task["pattern"], task["replace"]) if re.search(pattern, dir_file["file_name"]): save_name = ( re.sub(pattern, replace, dir_file["file_name"]) @@ -801,6 +729,81 @@ class Quark: return is_rename_count > 0 +class Emby: + def __init__(self, emby_url, emby_apikey): + self.is_active = False + if emby_url and emby_apikey: + self.emby_url = emby_url + self.emby_apikey = emby_apikey + if self.get_info(): + self.is_active = True + + def get_info(self): + url = f"{self.emby_url}/emby/System/Info" + headers = {"X-Emby-Token": self.emby_apikey} + querystring = {} + response = requests.request("GET", url, headers=headers, params=querystring) + if "application/json" in response.headers["Content-Type"]: + response = response.json() + print( + f"Emby媒体库: {response.get('ServerName','')} v{response.get('Version','')}" + ) + return True + else: + print(f"Emby媒体库: 连接失败❌ {response.text}") + return False + + def refresh(self, emby_id): + if emby_id: + url = f"{self.emby_url}/emby/Items/{emby_id}/Refresh" + headers = {"X-Emby-Token": self.emby_apikey} + querystring = { + "Recursive": "true", + "MetadataRefreshMode": "FullRefresh", + "ImageRefreshMode": "FullRefresh", + "ReplaceAllMetadata": "false", + "ReplaceAllImages": "false", + } + response = requests.request( + "POST", url, headers=headers, params=querystring + ) + if response.text == "": + print(f"🎞 刷新Emby媒体库:成功✅") + return True + else: + print(f"🎞 刷新Emby媒体库:{response.text}❌") + return False + + def search(self, media_name): + if media_name: + url = f"{self.emby_url}/emby/Items" + headers = {"X-Emby-Token": self.emby_apikey} + querystring = { + "IncludeItemTypes": "Series", + "StartIndex": 0, + "SortBy": "SortName", + "SortOrder": "Ascending", + "ImageTypeLimit": 0, + "Recursive": "true", + "SearchTerm": media_name, + "Limit": 10, + "IncludeSearchTypes": "false", + } + response = requests.request("GET", url, headers=headers, params=querystring) + if "application/json" in response.headers["Content-Type"]: + response = response.json() + if response.get("Items"): + for item in response["Items"]: + if item["IsFolder"]: + print( + f"🎞 《{item['Name']}》匹配到Emby媒体库ID:{item['Id']}" + ) + return item["Id"] + else: + print(f"🎞 搜索Emby媒体库:{response.text}❌") + return False + + def verify_account(account): # 验证账号 print(f"▶️ 验证第{account.index}个账号") @@ -845,10 +848,7 @@ def do_sign(account): sign_message = f"📅 执行签到: 今日签到+{int(sign_return/1024/1024)}MB,连签进度({growth_info['cap_sign']['sign_progress']+1}/{growth_info['cap_sign']['sign_target']})✅" message = f"{sign_message}\n{growth_message}" if ( - str( - CONFIG_DATA.get("push_config", {}).get("QUARK_SIGN_NOTIFY") - ).lower() - == "false" + CONFIG_DATA.get("push_config", {}).get("QUARK_SIGN_NOTIFY") == False or os.environ.get("QUARK_SIGN_NOTIFY") == "false" ): print(message) @@ -861,9 +861,9 @@ def do_sign(account): def do_save(account, tasklist=[]): - print(f"🧩 载入插件") - plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins( - CONFIG_DATA.get("plugins", {}) + emby = Emby( + CONFIG_DATA.get("emby", {}).get("url", ""), + CONFIG_DATA.get("emby", {}).get("apikey", ""), ) print(f"转存账号: {account.nickname}") # 获取全部保存目录fid @@ -882,55 +882,53 @@ def do_save(account, tasklist=[]): or (datetime.today().weekday() + 1 in task.get("runweek")) ) + total_files_transferred = 0 # 计数器初始化 + # 执行任务 for index, task in enumerate(tasklist): # 判断任务期限 if check_date(task): + if total_files_transferred >= 2: # 检查是否已转存超过2个文件 + print("⚠️ 已达到转存文件总数上限,停止转存任务") + break + print() print(f"#{index+1}------------------") print(f"任务名称: {task['taskname']}") print(f"分享链接: {task['shareurl']}") - print(f"保存路径: {task['savepath']}") + print(f"目标目录: {task['savepath']}") print(f"正则匹配: {task['pattern']}") print(f"正则替换: {task['replace']}") if task.get("enddate"): print(f"任务截止: {task['enddate']}") + if task.get("emby_id"): + print(f"刷媒体库: {task['emby_id']}") if task.get("ignore_extension"): print(f"忽略后缀: {task['ignore_extension']}") if task.get("update_subdir"): print(f"更子目录: {task['update_subdir']}") print() - is_new_tree = account.do_save_task(task) + + is_new = account.do_save_task(task) is_rename = account.do_rename_task(task) - # 补充任务的插件配置 - def merge_dicts(a, b): - result = a.copy() - for key, value in b.items(): - if ( - key in result - and isinstance(result[key], dict) - and isinstance(value, dict) - ): - result[key] = merge_dicts(result[key], value) - elif key not in result: - result[key] = value - return result + if is_new or is_rename: + total_files_transferred += 1 # 每成功转存或重命名一个文件,计数器加1 + + # 刷新媒体库 + if emby.is_active and (is_new or is_rename) and task.get("emby_id") != "0": + if task.get("emby_id"): + emby.refresh(task["emby_id"]) + else: + match_emby_id = emby.search(task["taskname"]) + if match_emby_id: + task["emby_id"] = match_emby_id + emby.refresh(match_emby_id) - task["addition"] = merge_dicts( - task.get("addition", {}), task_plugins_config - ) - # 调用插件 - if is_new_tree or is_rename: - print(f"🧩 调用插件") - for plugin_name, plugin in plugins.items(): - if plugin.is_active and (is_new_tree or is_rename): - task = ( - plugin.run(task, account=account, tree=is_new_tree) or task - ) print() + def main(): global CONFIG_DATA start_time = datetime.now() @@ -951,20 +949,19 @@ def main(): else: print(f"⚙️ 配置文件 {config_path} 不存在❌,正远程从下载配置模版") config_url = f"{GH_PROXY}https://raw.githubusercontent.com/Cp0204/quark_auto_save/main/quark_config.json" - if Config.download_file(config_url, config_path): + if download_file(config_url, config_path): print("⚙️ 配置模版下载成功✅,请到程序目录中手动配置") return else: print(f"⚙️ 正从 {config_path} 文件中读取配置") with open(config_path, "r", encoding="utf-8") as file: CONFIG_DATA = json.load(file) - Config.breaking_change_update(CONFIG_DATA) cookie_val = CONFIG_DATA.get("cookie") if not CONFIG_DATA.get("magic_regex"): CONFIG_DATA["magic_regex"] = MAGIC_REGEX cookie_form_file = True # 获取cookie - cookies = Config.get_cookies(cookie_val) + cookies = get_cookies(cookie_val) if not cookies: print("❌ cookie 未配置") return @@ -997,7 +994,7 @@ def main(): if cookie_form_file: # 更新配置 with open(config_path, "w", encoding="utf-8") as file: - json.dump(CONFIG_DATA, file, ensure_ascii=False, sort_keys=False, indent=2) + json.dump(CONFIG_DATA, file, ensure_ascii=False, indent=2) print(f"===============程序结束===============") duration = datetime.now() - start_time