mirror of
https://github.com/Bao-qing/123pan.git
synced 2026-02-12 13:00:31 +08:00
前后分离
This commit is contained in:
parent
aa9d54390f
commit
18fcfc2c01
400
pan123_cli.py
Normal file
400
pan123_cli.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
"""
|
||||
123pan 控制台交互界面 —— 仅负责用户 IO,所有业务调用 Pan123Core。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from pan123_core import Pan123Core, format_size, make_result
|
||||
|
||||
|
||||
# ──────────────── 颜色工具 ────────────────
|
||||
|
||||
class Color:
|
||||
"""ANSI 颜色常量"""
|
||||
RESET = "\033[0m"
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
PURPLE = "\033[35m"
|
||||
CYAN = "\033[96m"
|
||||
|
||||
|
||||
def colored(text: str, color: str) -> str:
|
||||
return f"{color}{text}{Color.RESET}"
|
||||
|
||||
|
||||
# ──────────────── CLI 类 ────────────────
|
||||
|
||||
class Pan123CLI:
|
||||
"""控制台交互界面"""
|
||||
|
||||
HELP_TEXT = """可用命令:
|
||||
ls - 显示当前目录
|
||||
cd [编号|..|/] - 切换目录
|
||||
mkdir [名称] - 创建目录
|
||||
upload [路径] - 上传文件
|
||||
rm [编号] - 删除文件
|
||||
share [编号 ...] - 创建分享
|
||||
link [编号] - 获取文件直链
|
||||
download/d [编号] - 下载文件
|
||||
recycle - 管理回收站
|
||||
refresh/re - 刷新目录
|
||||
reload - 重新加载配置并刷新
|
||||
login - 登录
|
||||
logout - 登出并清除 token
|
||||
clearaccount - 清除已登录账号(包括用户名和密码)
|
||||
more - 继续加载更多文件
|
||||
protocol [android|web] - 切换协议
|
||||
exit - 退出程序"""
|
||||
|
||||
def __init__(self, config_file: str = "123pan_config.json"):
|
||||
self.core = Pan123Core(config_file=config_file)
|
||||
self._download_mode: int = 0 # 0=询问, 3=全部覆盖, 4=全部跳过
|
||||
|
||||
# ──────────────── 启动 ────────────────
|
||||
|
||||
def run(self) -> None:
|
||||
"""主入口"""
|
||||
# Windows cmd 颜色支持
|
||||
if os.name == "nt":
|
||||
os.system("")
|
||||
|
||||
self._print_banner()
|
||||
self._init_login()
|
||||
|
||||
while True:
|
||||
try:
|
||||
prompt = colored(f"{self.core.cwd_path}>", Color.RED) + " "
|
||||
command = input(prompt).strip()
|
||||
if not command:
|
||||
continue
|
||||
self._dispatch(command)
|
||||
except KeyboardInterrupt:
|
||||
print("\n操作已取消")
|
||||
except EOFError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(colored(f"发生错误: {e}", Color.RED))
|
||||
|
||||
# ──────────────── 初始化 ────────────────
|
||||
|
||||
def _print_banner(self) -> None:
|
||||
print("=" * 60)
|
||||
print("123网盘客户端".center(56))
|
||||
print("=" * 60)
|
||||
|
||||
def _init_login(self) -> None:
|
||||
"""尝试加载配置 -> 尝试访问目录 -> 必要时登录"""
|
||||
self.core.load_config()
|
||||
|
||||
r = self.core.refresh()
|
||||
if r["code"] != 0:
|
||||
# 需要登录
|
||||
if not self.core.user_name:
|
||||
self.core.user_name = input("请输入用户名: ")
|
||||
if not self.core.password:
|
||||
self.core.password = input("请输入密码: ")
|
||||
lr = self.core.login()
|
||||
self._print_result(lr)
|
||||
if lr["code"] == 0:
|
||||
self.core.refresh()
|
||||
|
||||
self._show_files()
|
||||
|
||||
# ──────────────── 命令分发 ────────────────
|
||||
|
||||
def _dispatch(self, command: str) -> None:
|
||||
parts = command.split(maxsplit=1)
|
||||
cmd = parts[0].lower()
|
||||
arg = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
handler = {
|
||||
"ls": lambda: self._show_files(),
|
||||
"login": lambda: self._do_login(),
|
||||
"logout": lambda: self._do_logout(),
|
||||
"clearaccount": lambda: self._do_clear_account(),
|
||||
"exit": lambda: sys.exit(0),
|
||||
"cd": lambda: self._do_cd(arg),
|
||||
"mkdir": lambda: self._do_mkdir(arg),
|
||||
"upload": lambda: self._do_upload(arg),
|
||||
"rm": lambda: self._do_rm(arg),
|
||||
"share": lambda: self._do_share(arg),
|
||||
"more": lambda: self._do_more(),
|
||||
"link": lambda: self._do_link(arg),
|
||||
"download": lambda: self._do_download(arg),
|
||||
"d": lambda: self._do_download(arg),
|
||||
"recycle": lambda: self._do_recycle(),
|
||||
"refresh": lambda: self._do_refresh(),
|
||||
"re": lambda: self._do_refresh(),
|
||||
"reload": lambda: self._do_reload(),
|
||||
"protocol": lambda: self._do_protocol(arg),
|
||||
"help": lambda: print(self.HELP_TEXT),
|
||||
}.get(cmd)
|
||||
|
||||
if handler:
|
||||
handler()
|
||||
elif cmd.isdigit():
|
||||
self._do_select(int(cmd))
|
||||
else:
|
||||
print(self.HELP_TEXT)
|
||||
|
||||
# ──────────────── 显示 ────────────────
|
||||
|
||||
def _show_files(self) -> None:
|
||||
items = self.core.file_list
|
||||
if not items:
|
||||
print("当前目录为空")
|
||||
return
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"当前路径: {self.core.cwd_path}")
|
||||
print("-" * 60)
|
||||
print(f"{'编号':<6}{'类型':<8}{'大小':<12}{'名称'}")
|
||||
print("-" * 60)
|
||||
for idx, item in enumerate(items, 1):
|
||||
is_dir = item["Type"] == 1
|
||||
type_str = "文件夹" if is_dir else "文件"
|
||||
size_str = format_size(item["Size"])
|
||||
color = Color.PURPLE if is_dir else Color.YELLOW
|
||||
print(colored(f"{idx:<6}{type_str:<8}{size_str:<12}{item['FileName']}", color))
|
||||
if not self.core.all_loaded:
|
||||
remaining = self.core.file_total - len(items)
|
||||
print(f"\n还有 {remaining} 个文件未加载,输入 'more' 继续加载")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
def _print_result(self, r: dict) -> None:
|
||||
"""打印 Result 消息"""
|
||||
if r["code"] == 0:
|
||||
print(colored(r["message"], Color.GREEN))
|
||||
else:
|
||||
print(colored(f"[错误 {r['code']}] {r['message']}", Color.RED))
|
||||
|
||||
# ──────────────── 命令实现 ────────────────
|
||||
|
||||
def _do_login(self) -> None:
|
||||
if not self.core.user_name:
|
||||
self.core.user_name = input("请输入用户名: ")
|
||||
if not self.core.password:
|
||||
self.core.password = input("请输入密码: ")
|
||||
r = self.core.login()
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
self._do_refresh()
|
||||
|
||||
def _do_logout(self) -> None:
|
||||
r = self.core.logout()
|
||||
self._print_result(r)
|
||||
|
||||
def _do_clear_account(self) -> None:
|
||||
"""清除已登录账号:清除用户名、密码、token 等信息"""
|
||||
confirm = input("确定要清除已登录账号信息吗?(y/N): ").strip().lower()
|
||||
if confirm == 'y':
|
||||
r = self.core.clear_account()
|
||||
self._print_result(r)
|
||||
else:
|
||||
print("操作已取消")
|
||||
|
||||
def _do_cd(self, arg: str) -> None:
|
||||
if arg == "..":
|
||||
r = self.core.cd_up()
|
||||
elif arg == "/":
|
||||
r = self.core.cd_root()
|
||||
elif arg.isdigit():
|
||||
r = self.core.cd(int(arg) - 1)
|
||||
else:
|
||||
print("用法: cd [编号|..|/]")
|
||||
return
|
||||
if r["code"] != 0:
|
||||
self._print_result(r)
|
||||
else:
|
||||
self._show_files()
|
||||
|
||||
def _do_mkdir(self, name: str) -> None:
|
||||
if not name:
|
||||
name = input("请输入目录名: ")
|
||||
r = self.core.mkdir(name)
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
self._do_refresh()
|
||||
|
||||
def _do_upload(self, path: str) -> None:
|
||||
if not path:
|
||||
path = input("请输入文件路径: ")
|
||||
r = self.core.upload_file(path, on_progress=self._upload_progress)
|
||||
if r["code"] == 5060:
|
||||
choice = input("检测到同名文件,输入 1 覆盖,2 保留两者,其他取消: ")
|
||||
if choice == "1":
|
||||
r = self.core.upload_file(path, duplicate=1, on_progress=self._upload_progress)
|
||||
elif choice == "2":
|
||||
r = self.core.upload_file(path, duplicate=2, on_progress=self._upload_progress)
|
||||
else:
|
||||
print("上传取消")
|
||||
return
|
||||
print() # 换行
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
self._do_refresh()
|
||||
|
||||
def _do_rm(self, arg: str) -> None:
|
||||
if not arg.isdigit():
|
||||
print("请提供文件编号")
|
||||
return
|
||||
r = self.core.trash_by_index(int(arg) - 1)
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
self._do_refresh()
|
||||
|
||||
def _do_share(self, arg: str) -> None:
|
||||
indices = [int(x) - 1 for x in arg.split() if x.isdigit()]
|
||||
if not indices:
|
||||
print("请提供文件编号")
|
||||
return
|
||||
# 显示待分享的文件
|
||||
names = [self.core.file_list[i]["FileName"] for i in indices if 0 <= i < len(self.core.file_list)]
|
||||
print("分享文件:", ", ".join(names))
|
||||
pwd = input("输入提取码(留空跳过): ").strip()
|
||||
r = self.core.share_by_indices(indices, pwd)
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
print(f"链接: {r['data']['share_url']}")
|
||||
if r["data"]["share_pwd"]:
|
||||
print(f"提取码: {r['data']['share_pwd']}")
|
||||
|
||||
def _do_more(self) -> None:
|
||||
r = self.core.load_more()
|
||||
if r["code"] != 0:
|
||||
self._print_result(r)
|
||||
else:
|
||||
self._show_files()
|
||||
|
||||
def _do_link(self, arg: str) -> None:
|
||||
if not arg.isdigit():
|
||||
print("请提供文件编号")
|
||||
return
|
||||
r = self.core.get_download_url(int(arg) - 1)
|
||||
if r["code"] == 0:
|
||||
print(f"文件直链: {r['data']['url']}")
|
||||
else:
|
||||
self._print_result(r)
|
||||
|
||||
def _do_download(self, arg: str) -> None:
|
||||
if not arg.isdigit():
|
||||
print("请提供文件编号")
|
||||
return
|
||||
idx = int(arg) - 1
|
||||
if not (0 <= idx < len(self.core.file_list)):
|
||||
print("无效的文件编号")
|
||||
return
|
||||
item = self.core.file_list[idx]
|
||||
print(f"开始下载: {item['FileName']}")
|
||||
|
||||
overwrite = self._download_mode == 3
|
||||
skip = self._download_mode == 4
|
||||
r = self.core.download_file(
|
||||
idx,
|
||||
on_progress=self._download_progress,
|
||||
overwrite=overwrite,
|
||||
skip_existing=skip,
|
||||
)
|
||||
# 冲突处理
|
||||
if r["code"] == 1 and r.get("data", {}).get("conflict"):
|
||||
print(f"文件已存在: {item['FileName']}")
|
||||
choice = input("输入 1 覆盖,2 跳过,3 全部覆盖,4 全部跳过: ").strip()
|
||||
if choice == "4":
|
||||
self._download_mode = 4
|
||||
print("跳过下载")
|
||||
return
|
||||
elif choice == "2":
|
||||
print("跳过下载")
|
||||
return
|
||||
elif choice == "3":
|
||||
self._download_mode = 3
|
||||
r = self.core.download_file(idx, on_progress=self._download_progress, overwrite=True)
|
||||
|
||||
print() # 换行
|
||||
self._print_result(r)
|
||||
|
||||
def _do_recycle(self) -> None:
|
||||
r = self.core.list_recycle()
|
||||
if r["code"] != 0:
|
||||
self._print_result(r)
|
||||
return
|
||||
items = r["data"]
|
||||
if not items:
|
||||
print("回收站为空")
|
||||
return
|
||||
print("\n回收站内容:")
|
||||
for i, item in enumerate(items, 1):
|
||||
print(f" {i}. {item['FileName']} ({format_size(item['Size'])})")
|
||||
action = input("\n输入编号恢复文件,或输入 'clear' 清空回收站: ").strip()
|
||||
if action.isdigit():
|
||||
idx = int(action) - 1
|
||||
if 0 <= idx < len(items):
|
||||
rr = self.core.restore(items[idx]["FileId"])
|
||||
self._print_result(rr)
|
||||
else:
|
||||
print("无效编号")
|
||||
elif action == "clear":
|
||||
for item in items:
|
||||
self.core.trash(item, delete=True)
|
||||
print("回收站已清空")
|
||||
self._do_refresh()
|
||||
|
||||
def _do_refresh(self) -> None:
|
||||
self._download_mode = 0
|
||||
r = self.core.refresh()
|
||||
if r["code"] != 0:
|
||||
self._print_result(r)
|
||||
else:
|
||||
self._show_files()
|
||||
|
||||
def _do_reload(self) -> None:
|
||||
r = self.core.load_config()
|
||||
self._print_result(r)
|
||||
self._do_refresh()
|
||||
|
||||
def _do_protocol(self, arg: str) -> None:
|
||||
if arg.lower() not in ("android", "web"):
|
||||
print("请指定协议: android 或 web")
|
||||
return
|
||||
r = self.core.set_protocol(arg.lower())
|
||||
self._print_result(r)
|
||||
if r["code"] == 0:
|
||||
self._do_refresh()
|
||||
|
||||
def _do_select(self, num: int) -> None:
|
||||
"""数字选择:文件夹进入,文件下载"""
|
||||
idx = num - 1
|
||||
if not (0 <= idx < len(self.core.file_list)):
|
||||
print("无效的文件编号")
|
||||
return
|
||||
if self.core.file_list[idx]["Type"] == 1:
|
||||
self._do_cd(str(num))
|
||||
else:
|
||||
self._do_download(str(num))
|
||||
|
||||
# ──────────────── 进度回调 ────────────────
|
||||
|
||||
@staticmethod
|
||||
def _download_progress(downloaded: int, total: int, speed: float) -> None:
|
||||
if total > 0:
|
||||
pct = downloaded / total * 100
|
||||
print(
|
||||
f"\r进度: {pct:.1f}% | {format_size(downloaded)}/{format_size(total)} | {format_size(int(speed))}/s",
|
||||
end=" ",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _upload_progress(uploaded: int, total: int) -> None:
|
||||
if total > 0:
|
||||
pct = uploaded / total * 100
|
||||
print(f"\r上传进度: {pct:.1f}%", end="", flush=True)
|
||||
|
||||
|
||||
# ──────────────── 入口 ────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
Pan123CLI().run()
|
||||
1285
pan123_core.py
Normal file
1285
pan123_core.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user