分模块
This commit is contained in:
parent
b84358f228
commit
2aa4d0870b
352
PikPakFileSystem.py
Normal file
352
PikPakFileSystem.py
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import httpx
|
||||||
|
from pikpakapi import PikPakApi, DownloadStatus
|
||||||
|
from typing import Dict
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class NodeBase:
|
||||||
|
def __init__(self, id : str, name : str, fatherId : str):
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self._father_id = fatherId
|
||||||
|
self.lastUpdate : datetime = None
|
||||||
|
|
||||||
|
class DirNode(NodeBase):
|
||||||
|
def __init__(self, id : str, name : str, fatherId : str):
|
||||||
|
super().__init__(id, name, fatherId)
|
||||||
|
self.children_id : list[str] = []
|
||||||
|
|
||||||
|
class FileNode(NodeBase):
|
||||||
|
def __init__(self, id : str, name : str, fatherId : str):
|
||||||
|
super().__init__(id, name, fatherId)
|
||||||
|
self.url : str = None
|
||||||
|
|
||||||
|
class PikPakFileSystem:
|
||||||
|
#region 内部接口
|
||||||
|
def __init__(self, auth_cache_path : str = None, proxy_address : str = None, root_id : str = None):
|
||||||
|
# 初始化虚拟文件节点
|
||||||
|
self._nodes : Dict[str, NodeBase] = {}
|
||||||
|
self._root : DirNode = DirNode(root_id, "", None)
|
||||||
|
self._cwd : DirNode = self._root
|
||||||
|
|
||||||
|
# 初始化鉴权和代理信息
|
||||||
|
self._auth_cache_path : str = auth_cache_path
|
||||||
|
self.proxy_address : str = proxy_address
|
||||||
|
self._pikpak_client : PikPakApi = None
|
||||||
|
self._try_login_from_cache()
|
||||||
|
|
||||||
|
|
||||||
|
#region 鉴权信息相关
|
||||||
|
class PikPakToken:
|
||||||
|
def __init__(self, username : str, password : str, access_token : str, refresh_token : str, user_id : str):
|
||||||
|
self.username : str = username
|
||||||
|
self.password : str = password
|
||||||
|
self.access_token : str = access_token
|
||||||
|
self.refresh_token : str = refresh_token
|
||||||
|
self.user_id : str = user_id
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self.__dict__)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str):
|
||||||
|
data = json.loads(json_str)
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
def _init_client_by_token(self, token : PikPakToken) -> None:
|
||||||
|
self._init_client_by_username_and_password(token.username, token.password)
|
||||||
|
self._pikpak_client.access_token = token.access_token
|
||||||
|
self._pikpak_client.refresh_token = token.refresh_token
|
||||||
|
self._pikpak_client.user_id = token.user_id
|
||||||
|
self._pikpak_client.encode_token()
|
||||||
|
|
||||||
|
def _init_client_by_username_and_password(self, username : str, password : str) -> None:
|
||||||
|
httpx_client_args : Dict[str, Any] = None
|
||||||
|
if self.proxy_address != None:
|
||||||
|
httpx_client_args = {
|
||||||
|
"proxy": self.proxy_address,
|
||||||
|
"transport": httpx.AsyncHTTPTransport()
|
||||||
|
}
|
||||||
|
|
||||||
|
self._pikpak_client = PikPakApi(
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
httpx_client_args=httpx_client_args)
|
||||||
|
|
||||||
|
def _try_login_from_cache(self) -> None:
|
||||||
|
if self._auth_cache_path is None:
|
||||||
|
return
|
||||||
|
if not os.path.exists(self._auth_cache_path):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(self._auth_cache_path, 'r', encoding='utf-8') as file:
|
||||||
|
content : str = file.read()
|
||||||
|
token : PikPakFileSystem.PikPakToken = PikPakFileSystem.PikPakToken.from_json(content)
|
||||||
|
self._init_client_by_token(token)
|
||||||
|
logging.info("successfully load login info from cache")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"failed to load login info from cache, exception occurred: {e}")
|
||||||
|
|
||||||
|
def _dump_login_info(self) -> None:
|
||||||
|
if self._auth_cache_path is None:
|
||||||
|
return
|
||||||
|
with open(self._auth_cache_path, 'w', encoding='utf-8') as file:
|
||||||
|
token : PikPakFileSystem.PikPakToken = PikPakFileSystem.PikPakToken(self._pikpak_client.username, self._pikpak_client.password, self._pikpak_client.access_token, self._pikpak_client.refresh_token, self._pikpak_client.user_id)
|
||||||
|
file.write(token.to_json())
|
||||||
|
logging.info("successfully dump login info to cache")
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件系统相关
|
||||||
|
class PathWalker():
|
||||||
|
def __init__(self, path : str, sep : str = "/"):
|
||||||
|
self._path_spots : list[str] = []
|
||||||
|
if not path.startswith(sep):
|
||||||
|
self._path_spots.append(".")
|
||||||
|
path_spots : list[str] = path.split(sep)
|
||||||
|
self._path_spots.extend(path_spots)
|
||||||
|
|
||||||
|
def IsAbsolute(self) -> bool:
|
||||||
|
return len(self._path_spots) == 0 or self._path_spots[0] != "."
|
||||||
|
|
||||||
|
def AppendSpot(self, spot) -> None:
|
||||||
|
self._path_spots.append(spot)
|
||||||
|
|
||||||
|
def Walk(self) -> list[str]:
|
||||||
|
return self._path_spots
|
||||||
|
|
||||||
|
async def _get_node_by_id(self, id : str) -> NodeBase:
|
||||||
|
if id == self._root.id:
|
||||||
|
return self._root
|
||||||
|
if id not in self._nodes:
|
||||||
|
return None
|
||||||
|
return self._nodes[id]
|
||||||
|
|
||||||
|
async def _get_father_node(self, node : NodeBase) -> NodeBase:
|
||||||
|
if node is self._root:
|
||||||
|
return self._root
|
||||||
|
return await self._get_node_by_id(node._father_id)
|
||||||
|
|
||||||
|
async def _add_node(self, node : NodeBase) -> None:
|
||||||
|
self._nodes[node.id] = node
|
||||||
|
father = await self._get_father_node(node)
|
||||||
|
if father is not None and isinstance(father, DirNode):
|
||||||
|
father.children_id.append(node.id)
|
||||||
|
|
||||||
|
async def _remove_node(self, node : NodeBase) -> None:
|
||||||
|
father = await self._get_father_node(node)
|
||||||
|
if father is not None and isinstance(father, DirNode):
|
||||||
|
father.children_id.remove(node.id)
|
||||||
|
self._nodes.pop(node.id)
|
||||||
|
|
||||||
|
async def _find_child_in_dir_by_name(self, dir : DirNode, name : str) -> NodeBase:
|
||||||
|
if dir is self._root and name == "":
|
||||||
|
return self._root
|
||||||
|
for child_id in dir.children_id:
|
||||||
|
node = await self._get_node_by_id(child_id)
|
||||||
|
if node.name == name:
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _refresh(self, node : NodeBase):
|
||||||
|
if isinstance(node, DirNode):
|
||||||
|
if node.lastUpdate != None:
|
||||||
|
return
|
||||||
|
next_page_token : str = None
|
||||||
|
children_info : list[Dict[str, Any]] = []
|
||||||
|
while True:
|
||||||
|
dir_info : Dict[str, Any] = await self._pikpak_client.file_list(parent_id = node.id, next_page_token=next_page_token)
|
||||||
|
next_page_token = dir_info["next_page_token"]
|
||||||
|
children_info.extend(dir_info["files"])
|
||||||
|
if next_page_token is None or next_page_token == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
node.children_id.clear()
|
||||||
|
for child_info in children_info:
|
||||||
|
id : str = child_info["id"]
|
||||||
|
name : str = child_info["name"]
|
||||||
|
|
||||||
|
child : NodeBase = await self._get_node_by_id(id)
|
||||||
|
if child is None:
|
||||||
|
if child_info["kind"].endswith("folder"):
|
||||||
|
child = DirNode(id, name, node.id)
|
||||||
|
else:
|
||||||
|
child = FileNode(id, name, node.id)
|
||||||
|
child.name = name
|
||||||
|
child._father_id = node.id
|
||||||
|
await self._add_node(child)
|
||||||
|
elif isinstance(node, FileNode):
|
||||||
|
result = await self._pikpak_client.get_download_url(node.id)
|
||||||
|
node.url = result["web_content_link"]
|
||||||
|
|
||||||
|
node.lastUpdate = datetime.now()
|
||||||
|
|
||||||
|
async def _path_to_node(self, path : str) -> NodeBase:
|
||||||
|
father, son_name = await self._path_to_father_node_and_son_name(path)
|
||||||
|
if son_name == "":
|
||||||
|
return father
|
||||||
|
if isinstance(father, DirNode):
|
||||||
|
return await self._find_child_in_dir_by_name(father, son_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _path_to_father_node_and_son_name(self, path : str) -> tuple[NodeBase, str]:
|
||||||
|
path_walker : PikPakFileSystem.PathWalker = PikPakFileSystem.PathWalker(path)
|
||||||
|
father : NodeBase = None
|
||||||
|
son_name : str = None
|
||||||
|
current : NodeBase = self._root if path_walker.IsAbsolute() else self._cwd
|
||||||
|
|
||||||
|
for spot in path_walker.Walk():
|
||||||
|
if current is None:
|
||||||
|
father = None
|
||||||
|
break
|
||||||
|
if spot == "..":
|
||||||
|
current = await self._get_father_node(current)
|
||||||
|
continue
|
||||||
|
father = current
|
||||||
|
if not isinstance(current, DirNode):
|
||||||
|
current = None
|
||||||
|
continue
|
||||||
|
await self._refresh(current)
|
||||||
|
if spot == ".":
|
||||||
|
continue
|
||||||
|
sonName = spot
|
||||||
|
current = await self._find_child_in_dir_by_name(current, spot)
|
||||||
|
|
||||||
|
if current != None:
|
||||||
|
father = await self._get_father_node(current)
|
||||||
|
sonName = current.name
|
||||||
|
|
||||||
|
return father, sonName
|
||||||
|
|
||||||
|
async def _node_to_path(self, node : NodeBase) -> str:
|
||||||
|
if node is self._root:
|
||||||
|
return "/"
|
||||||
|
spots : list[str] = []
|
||||||
|
current = node
|
||||||
|
while current is not self._root:
|
||||||
|
spots.append(current.name)
|
||||||
|
current = await self._get_father_node(current)
|
||||||
|
spots.append("")
|
||||||
|
return "/".join(reversed(spots))
|
||||||
|
|
||||||
|
async def _is_ancestors_of(self, node_a : NodeBase, node_b : NodeBase) -> bool:
|
||||||
|
if node_b is node_a:
|
||||||
|
return False
|
||||||
|
if node_a is self._root:
|
||||||
|
return True
|
||||||
|
while node_b._father_id != self._root.id:
|
||||||
|
node_b = await self._get_father_node(node_b)
|
||||||
|
if node_b is node_a:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 对外接口
|
||||||
|
async def Login(self, username : str = None, password : str = None) -> None:
|
||||||
|
if self._pikpak_client != None and username is None and password is None:
|
||||||
|
username = self._pikpak_client.username
|
||||||
|
password = self._pikpak_client.password
|
||||||
|
|
||||||
|
if username == None and password == None:
|
||||||
|
raise Exception("Username and password are required")
|
||||||
|
|
||||||
|
self._init_client_by_username_and_password(username, password)
|
||||||
|
await self._pikpak_client.login()
|
||||||
|
self._dump_login_info()
|
||||||
|
|
||||||
|
async def IsDir(self, path : str) -> bool:
|
||||||
|
node = await self._path_to_node(path)
|
||||||
|
return isinstance(node, DirNode)
|
||||||
|
|
||||||
|
async def SplitPath(self, path : str) -> tuple[str, str]:
|
||||||
|
father, son_name = await self._path_to_father_node_and_son_name(path)
|
||||||
|
return await self._node_to_path(father), son_name
|
||||||
|
|
||||||
|
async def GetChildrenNames(self, path : str, ignore_files : bool) -> list[str]:
|
||||||
|
node = await self._path_to_node(path)
|
||||||
|
if not isinstance(node, DirNode):
|
||||||
|
return []
|
||||||
|
await self._refresh(node)
|
||||||
|
children_names : list[str] = []
|
||||||
|
for child_id in node.children_id:
|
||||||
|
child = await self._get_node_by_id(child_id)
|
||||||
|
if ignore_files and isinstance(child, FileNode):
|
||||||
|
continue
|
||||||
|
children_names.append(child.name)
|
||||||
|
return children_names
|
||||||
|
|
||||||
|
async def Delete(self, paths : list[str]) -> None:
|
||||||
|
nodes = [await self._path_to_node(path) for path in paths]
|
||||||
|
for node in nodes:
|
||||||
|
if await self._is_ancestors_of(node, self._cwd):
|
||||||
|
raise Exception("Cannot delete ancestors")
|
||||||
|
await self._pikpak_client.delete_to_trash([node.id for node in nodes])
|
||||||
|
for node in nodes:
|
||||||
|
await self._remove_node(node)
|
||||||
|
|
||||||
|
async def MakeDir(self, path : str) -> None:
|
||||||
|
father_path, son_name = await self._path_to_father_node_and_son_name(path)
|
||||||
|
father = await self._path_to_node(father_path)
|
||||||
|
result = await self._pikpak_client.create_folder(son_name, father.id)
|
||||||
|
id = result["file"]["id"]
|
||||||
|
name = result["file"]["name"]
|
||||||
|
son = DirNode(id, name, father.id)
|
||||||
|
await self._add_node(son)
|
||||||
|
|
||||||
|
async def SetCwd(self, path : str) -> None:
|
||||||
|
node = await self._path_to_node(path)
|
||||||
|
if not isinstance(node, DirNode):
|
||||||
|
raise Exception("Not a directory")
|
||||||
|
self._cwd = node
|
||||||
|
|
||||||
|
async def GetCwd(self) -> str:
|
||||||
|
return await self._node_to_path(self._cwd)
|
||||||
|
|
||||||
|
async def GetNodeIdByPath(self, path : str) -> str:
|
||||||
|
node = await self._path_to_node(path)
|
||||||
|
if node is None:
|
||||||
|
return None
|
||||||
|
return node.id
|
||||||
|
|
||||||
|
async def IfExists(self, path : str) -> bool:
|
||||||
|
return await self._path_to_node(path) is not None
|
||||||
|
|
||||||
|
async def ToFullPath(self, path : str) -> str:
|
||||||
|
node = await self._path_to_node(path)
|
||||||
|
return await self._node_to_path(node)
|
||||||
|
|
||||||
|
async def RemoteDownload(self, torrent : str, remote_base_path : str) -> tuple[str, str]:
|
||||||
|
node = await self._path_to_node(remote_base_path)
|
||||||
|
info = await self._pikpak_client.offline_download(torrent, node.id)
|
||||||
|
return info["task"]["file_id"], info["task"]["id"]
|
||||||
|
|
||||||
|
async def QueryTaskStatus(self, task_id : str, node_id : str) -> DownloadStatus:
|
||||||
|
return await self._pikpak_client.get_task_status(task_id, node_id)
|
||||||
|
|
||||||
|
async def UpdateDirectory(self, path : str, son_id : str) -> str:
|
||||||
|
await self._path_to_node(path)
|
||||||
|
son_info = await self._pikpak_client.offline_file_info(son_id)
|
||||||
|
kind = son_info["kind"]
|
||||||
|
parent_id = son_info["parent_id"]
|
||||||
|
name = son_info["name"]
|
||||||
|
son : NodeBase = None
|
||||||
|
if kind.endswith("folder"):
|
||||||
|
son = DirNode(son_id, name, parent_id)
|
||||||
|
else:
|
||||||
|
son = FileNode(son_id, name, parent_id)
|
||||||
|
await self._add_node(son)
|
||||||
|
return await self._node_to_path(son)
|
||||||
|
|
||||||
|
async def JoinPath(self, father : str, son : str) -> str:
|
||||||
|
father_node = await self._path_to_node(father)
|
||||||
|
son_node = await self._find_child_in_dir_by_name(father_node, son)
|
||||||
|
if son_node is None:
|
||||||
|
raise Exception("Son not found")
|
||||||
|
return await self._node_to_path(son_node)
|
||||||
|
|
||||||
|
#endregion
|
508
PikPakFs.py
508
PikPakFs.py
@ -1,508 +0,0 @@
|
|||||||
import httpx
|
|
||||||
from pikpakapi import PikPakApi, DownloadStatus
|
|
||||||
from typing import Dict
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from enum import Enum
|
|
||||||
import asyncio
|
|
||||||
import shortuuid
|
|
||||||
from utils import PathWalker
|
|
||||||
from typing import Callable, Awaitable
|
|
||||||
import random
|
|
||||||
|
|
||||||
class TaskStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
RUNNING = "running"
|
|
||||||
DONE = "done"
|
|
||||||
ERROR = "error"
|
|
||||||
PAUSED = "paused"
|
|
||||||
|
|
||||||
class PikPakTaskStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
REMOTE_DOWNLOADING = "remote"
|
|
||||||
LOCAL_DOWNLOADING = "local"
|
|
||||||
DONE = "done"
|
|
||||||
|
|
||||||
class FileDownloadTaskStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
DOWNLOADING = "downloading"
|
|
||||||
DONE = "done"
|
|
||||||
|
|
||||||
class UnRecoverableError(Exception):
|
|
||||||
def __init__(self, message):
|
|
||||||
super().__init__(message)
|
|
||||||
|
|
||||||
class TaskBase:
|
|
||||||
def __init__(self, id : str, tag : str = "", maxConcurrentNumber = -1):
|
|
||||||
self.id : str = shortuuid.uuid() if id is None else id
|
|
||||||
self.tag : str = tag
|
|
||||||
self.maxConcurrentNumber : int = maxConcurrentNumber
|
|
||||||
self.name : str = ""
|
|
||||||
|
|
||||||
self._status : TaskStatus = TaskStatus.PENDING
|
|
||||||
|
|
||||||
self.worker : asyncio.Task = None
|
|
||||||
self.handler : Callable[..., Awaitable] = None
|
|
||||||
|
|
||||||
class PikPakTask(TaskBase):
|
|
||||||
TAG = "PikPakTask"
|
|
||||||
MAX_CONCURRENT_NUMBER = 5
|
|
||||||
|
|
||||||
def __init__(self, torrent : str, toDirId : str, nodeId : str = None, status : PikPakTaskStatus = PikPakTaskStatus.PENDING, id : str = None):
|
|
||||||
super().__init__(id, PikPakTask.TAG, PikPakTask.MAX_CONCURRENT_NUMBER)
|
|
||||||
self.status : PikPakTaskStatus = status
|
|
||||||
self.toDirId : str = toDirId
|
|
||||||
self.nodeId : str = nodeId
|
|
||||||
self.torrent : str = torrent # todo: 将torrent的附加参数去掉再加入
|
|
||||||
self.remoteTaskId : str = None
|
|
||||||
|
|
||||||
self.progress : str = ""
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, PikPakTask):
|
|
||||||
return self is other or self.nodeId == other.nodeId
|
|
||||||
return False
|
|
||||||
|
|
||||||
class FileDownloadTask(TaskBase):
|
|
||||||
TAG = "FileDownloadTask"
|
|
||||||
MAX_CONCURRENT_NUMBER = 5
|
|
||||||
|
|
||||||
def __init__(self, nodeId : str, PikPakTaskId : str, relativePath : str, status : FileDownloadTaskStatus = FileDownloadTaskStatus.PENDING, id : str = None):
|
|
||||||
super().__init__(id, FileDownloadTask.TAG, FileDownloadTask.MAX_CONCURRENT_NUMBER)
|
|
||||||
self.status : FileDownloadTaskStatus = status
|
|
||||||
self.PikPakTaskId : str = PikPakTaskId
|
|
||||||
self.nodeId : str = nodeId
|
|
||||||
self.relativePath : str = relativePath
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, FileDownloadTask):
|
|
||||||
return self is other or (self.nodeId == other.nodeId and self.PikPakTaskId == other.PikPakTaskId)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def TaskWorker(task : TaskBase):
|
|
||||||
try:
|
|
||||||
if task._status != TaskStatus.PENDING:
|
|
||||||
return
|
|
||||||
task._status = TaskStatus.RUNNING
|
|
||||||
await task.handler(task)
|
|
||||||
task._status = TaskStatus.DONE
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
task._status = TaskStatus.PAUSED
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"task failed, exception occurred: {e}")
|
|
||||||
task._status = TaskStatus.ERROR
|
|
||||||
|
|
||||||
async def TaskManager(taskQueues : Dict[str, list[TaskBase]]):
|
|
||||||
# todo: 处理取消的情况
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
for taskQueue in taskQueues.values():
|
|
||||||
notRunningTasks = [task for task in taskQueue if task.worker is None or task.worker.done()]
|
|
||||||
runningTasksNumber = len(taskQueue) - len(notRunningTasks)
|
|
||||||
for task in [task for task in notRunningTasks if task._status == TaskStatus.PENDING]:
|
|
||||||
if runningTasksNumber >= task.maxConcurrentNumber:
|
|
||||||
break
|
|
||||||
task.worker = asyncio.create_task(TaskWorker(task))
|
|
||||||
runningTasksNumber += 1
|
|
||||||
|
|
||||||
|
|
||||||
class FsNode:
|
|
||||||
def __init__(self, id : str, name : str, fatherId : str):
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.fatherId = fatherId
|
|
||||||
self.lastUpdate : datetime = None
|
|
||||||
|
|
||||||
class DirNode(FsNode):
|
|
||||||
def __init__(self, id : str, name : str, fatherId : str):
|
|
||||||
super().__init__(id, name, fatherId)
|
|
||||||
self.childrenId : list[str] = []
|
|
||||||
|
|
||||||
class FileNode(FsNode):
|
|
||||||
def __init__(self, id : str, name : str, fatherId : str):
|
|
||||||
super().__init__(id, name, fatherId)
|
|
||||||
self.url : str = None
|
|
||||||
|
|
||||||
def IsDir(node : FsNode) -> bool:
|
|
||||||
return isinstance(node, DirNode)
|
|
||||||
|
|
||||||
def IsFile(node : FsNode) -> bool:
|
|
||||||
return isinstance(node, FileNode)
|
|
||||||
|
|
||||||
class PikPakToken:
|
|
||||||
def __init__(self, username, password, access_token, refresh_token, user_id):
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.access_token = access_token
|
|
||||||
self.refresh_token = refresh_token
|
|
||||||
self.user_id = user_id
|
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
return json.dumps(self.__dict__)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_json(cls, json_str):
|
|
||||||
data = json.loads(json_str)
|
|
||||||
return cls(**data)
|
|
||||||
|
|
||||||
class PikPakFs:
|
|
||||||
|
|
||||||
async def _pikpak_task_pending(self, task : PikPakTask):
|
|
||||||
pikPakTaskInfo = await self.client.offline_download(task.torrent, task.toDirId)
|
|
||||||
task.remoteTaskId = pikPakTaskInfo["task"]["id"]
|
|
||||||
task.nodeId = pikPakTaskInfo["task"]["file_id"]
|
|
||||||
task.status = PikPakTaskStatus.REMOTE_DOWNLOADING
|
|
||||||
|
|
||||||
async def _pikpak_offline_downloading(self, task : PikPakTask):
|
|
||||||
waitTime = 3
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(waitTime)
|
|
||||||
status = await self.client.get_task_status(task.remoteTaskId, task.nodeId)
|
|
||||||
if status in {DownloadStatus.not_found, DownloadStatus.not_downloading, DownloadStatus.error}:
|
|
||||||
self.status = PikPakTaskStatus.PENDING
|
|
||||||
raise Exception(f"remote download failed, status: {status}")
|
|
||||||
elif status == DownloadStatus.done:
|
|
||||||
break
|
|
||||||
waitTime = waitTime * 1.5
|
|
||||||
|
|
||||||
fileInfo = await self.client.offline_file_info(file_id=task.nodeId)
|
|
||||||
task.toDirId = fileInfo["parent_id"]
|
|
||||||
task.name = fileInfo["name"]
|
|
||||||
if fileInfo["kind"].endswith("folder"):
|
|
||||||
self.nodes[task.nodeId] = DirNode(task.nodeId, task.name, task.toDirId)
|
|
||||||
else:
|
|
||||||
self.nodes[task.nodeId] = FileNode(task.nodeId, task.name, task.toDirId)
|
|
||||||
|
|
||||||
father = self.GetNodeById(task.toDirId)
|
|
||||||
if father.id is not None and task.nodeId not in father.childrenId:
|
|
||||||
father.childrenId.append(task.nodeId)
|
|
||||||
task.status = PikPakTaskStatus.LOCAL_DOWNLOADING
|
|
||||||
|
|
||||||
async def _pikpak_local_downloading(self, task : PikPakTask):
|
|
||||||
node = self.GetNodeById(task.nodeId)
|
|
||||||
if IsFile(node):
|
|
||||||
fileDownloadTask = FileDownloadTask(task.nodeId, task.id, self.NodeToPath(node, node))
|
|
||||||
fileDownloadTask.handler = self._file_download_task_handler
|
|
||||||
fileDownloadTask.name = task.name
|
|
||||||
self._add_task(fileDownloadTask)
|
|
||||||
elif IsDir(node):
|
|
||||||
# 使用广度优先遍历
|
|
||||||
queue : list[DirNode] = [node]
|
|
||||||
while len(queue) > 0:
|
|
||||||
current = queue.pop(0)
|
|
||||||
await self.Refresh(current)
|
|
||||||
for childId in current.childrenId:
|
|
||||||
child = self.GetNodeById(childId)
|
|
||||||
if IsDir(child):
|
|
||||||
queue.append(child)
|
|
||||||
elif IsFile(child):
|
|
||||||
fileDownloadTask = FileDownloadTask(childId, task.id, self.NodeToPath(child, node))
|
|
||||||
fileDownloadTask.handler = self._file_download_task_handler
|
|
||||||
fileDownloadTask.name = task.name
|
|
||||||
self._add_task(fileDownloadTask)
|
|
||||||
|
|
||||||
# 开始等待下载任务完成
|
|
||||||
while True:
|
|
||||||
fileDownloadTasks = self.taskQueues.get(FileDownloadTask.TAG)
|
|
||||||
myTasks = [myTask for myTask in fileDownloadTasks if myTask.PikPakTaskId == task.id]
|
|
||||||
allNumber = len(myTasks)
|
|
||||||
notCompletedNumber = 0
|
|
||||||
pausedNumber = 0
|
|
||||||
errorNumber = 0
|
|
||||||
for myTask in myTasks:
|
|
||||||
if myTask._status == TaskStatus.PAUSED:
|
|
||||||
pausedNumber += 1
|
|
||||||
if myTask._status == TaskStatus.ERROR:
|
|
||||||
errorNumber += 1
|
|
||||||
if myTask._status in {TaskStatus.PENDING, TaskStatus.RUNNING}:
|
|
||||||
notCompletedNumber += 1
|
|
||||||
|
|
||||||
runningNumber = allNumber - notCompletedNumber - pausedNumber - errorNumber
|
|
||||||
task.progress = f"{runningNumber}/{allNumber} ({pausedNumber}|{errorNumber})"
|
|
||||||
|
|
||||||
if notCompletedNumber > 0:
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
continue
|
|
||||||
if errorNumber > 0:
|
|
||||||
raise Exception("file download failed")
|
|
||||||
if pausedNumber > 0:
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
break
|
|
||||||
|
|
||||||
task.status = PikPakTaskStatus.DONE
|
|
||||||
|
|
||||||
|
|
||||||
async def _pikpak_task_handler(self, task : PikPakTask):
|
|
||||||
while True:
|
|
||||||
if task.status == PikPakTaskStatus.PENDING:
|
|
||||||
await self._pikpak_task_pending(task)
|
|
||||||
elif task.status == PikPakTaskStatus.REMOTE_DOWNLOADING:
|
|
||||||
await self._pikpak_offline_downloading(task)
|
|
||||||
elif task.status == PikPakTaskStatus.LOCAL_DOWNLOADING:
|
|
||||||
await self._pikpak_local_downloading(task)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
async def _file_download_task_handler(self, task : FileDownloadTask):
|
|
||||||
if random.randint(1, 5) == 2:
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _add_task(self, task : TaskBase):
|
|
||||||
if self.taskQueues.get(task.tag) is None:
|
|
||||||
self.taskQueues[task.tag] = []
|
|
||||||
taskQueue = self.taskQueues[task.tag]
|
|
||||||
for other in taskQueue:
|
|
||||||
if other == task:
|
|
||||||
if other._status != TaskStatus.DONE:
|
|
||||||
other._status = TaskStatus.PENDING
|
|
||||||
return
|
|
||||||
taskQueue.append(task)
|
|
||||||
|
|
||||||
async def GetTaskById(self, taskId : str) -> TaskBase:
|
|
||||||
for taskQueue in self.taskQueues.values():
|
|
||||||
for task in taskQueue:
|
|
||||||
if task.id == taskId:
|
|
||||||
return task
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def StopTask(self, taskId : str):
|
|
||||||
task = await self.GetTaskById(taskId)
|
|
||||||
if task is not None and task._status in {TaskStatus.PENDING, TaskStatus.RUNNING}:
|
|
||||||
if task.worker is not None:
|
|
||||||
task.worker.cancel()
|
|
||||||
task.worker = None
|
|
||||||
|
|
||||||
async def ResumeTask(self, taskId : str):
|
|
||||||
task = await self.GetTaskById(taskId)
|
|
||||||
if task is not None and task._status in {TaskStatus.PAUSED, TaskStatus.ERROR}:
|
|
||||||
task._status = TaskStatus.PENDING
|
|
||||||
|
|
||||||
def Start(self):
|
|
||||||
return asyncio.create_task(TaskManager(self.taskQueues))
|
|
||||||
|
|
||||||
def __init__(self, loginCachePath : str = None, proxy : str = None, rootId = None):
|
|
||||||
self.nodes : Dict[str, FsNode] = {}
|
|
||||||
self.root = DirNode(rootId, "", None)
|
|
||||||
self.currentLocation = self.root
|
|
||||||
|
|
||||||
self.taskQueues : Dict[str, list[TaskBase]] = {}
|
|
||||||
self.loginCachePath = loginCachePath
|
|
||||||
self.proxyConfig = proxy
|
|
||||||
self.client : PikPakApi = None
|
|
||||||
self._try_login_from_cache()
|
|
||||||
|
|
||||||
def _init_client_by_token(self, token : PikPakToken):
|
|
||||||
self._init_client_by_username_and_password(token.username, token.password)
|
|
||||||
self.client.access_token = token.access_token
|
|
||||||
self.client.refresh_token = token.refresh_token
|
|
||||||
self.client.user_id = token.user_id
|
|
||||||
self.client.encode_token()
|
|
||||||
|
|
||||||
def _init_client_by_username_and_password(self, username : str, password : str):
|
|
||||||
httpx_client_args = None
|
|
||||||
if self.proxyConfig != None:
|
|
||||||
httpx_client_args = {
|
|
||||||
"proxy": self.proxyConfig,
|
|
||||||
"transport": httpx.AsyncHTTPTransport()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client = PikPakApi(
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
httpx_client_args=httpx_client_args)
|
|
||||||
|
|
||||||
def _try_login_from_cache(self):
|
|
||||||
if self.loginCachePath is None:
|
|
||||||
return
|
|
||||||
if not os.path.exists(self.loginCachePath):
|
|
||||||
return
|
|
||||||
with open(self.loginCachePath, 'r', encoding='utf-8') as file:
|
|
||||||
content = file.read()
|
|
||||||
token = PikPakToken.from_json(content)
|
|
||||||
self._init_client_by_token(token)
|
|
||||||
logging.info("successfully load login info from cache")
|
|
||||||
|
|
||||||
def _dump_login_info(self):
|
|
||||||
if self.loginCachePath is None:
|
|
||||||
return
|
|
||||||
with open(self.loginCachePath, 'w', encoding='utf-8') as file:
|
|
||||||
token = PikPakToken(self.client.username, self.client.password, self.client.access_token, self.client.refresh_token, self.client.user_id)
|
|
||||||
file.write(token.to_json())
|
|
||||||
logging.info("successfully dump login info to cache")
|
|
||||||
|
|
||||||
def _is_ancestors_of(self, nodeA : FsNode, nodeB : FsNode) -> bool:
|
|
||||||
if nodeB is nodeA:
|
|
||||||
return False
|
|
||||||
if nodeA is self.root:
|
|
||||||
return True
|
|
||||||
while nodeB.fatherId != self.root.id:
|
|
||||||
nodeB = self.nodes[nodeB.fatherId]
|
|
||||||
if nodeB is nodeA:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def GetNodeById(self, id : str) -> FsNode:
|
|
||||||
if id == self.root.id:
|
|
||||||
return self.root
|
|
||||||
if id not in self.nodes:
|
|
||||||
return None
|
|
||||||
return self.nodes[id]
|
|
||||||
|
|
||||||
def GetFatherNode(self, node : FsNode) -> FsNode:
|
|
||||||
if node is self.root:
|
|
||||||
return self.root
|
|
||||||
return self.GetNodeById(node.fatherId)
|
|
||||||
|
|
||||||
def FindChildInDirByName(self, dir : DirNode, name : str):
|
|
||||||
if dir is self.root and name == "":
|
|
||||||
return self.root
|
|
||||||
for childId in dir.childrenId:
|
|
||||||
node = self.nodes[childId]
|
|
||||||
if name == node.name:
|
|
||||||
return node
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def Refresh(self, node : FsNode):
|
|
||||||
if IsDir(node):
|
|
||||||
if node.lastUpdate != None:
|
|
||||||
return
|
|
||||||
next_page_token = None
|
|
||||||
childrenInfo = []
|
|
||||||
while True:
|
|
||||||
dirInfo = await self.client.file_list(parent_id = node.id, next_page_token=next_page_token)
|
|
||||||
next_page_token = dirInfo["next_page_token"]
|
|
||||||
currentPageNodes = dirInfo["files"]
|
|
||||||
childrenInfo.extend(currentPageNodes)
|
|
||||||
if next_page_token is None or next_page_token == "":
|
|
||||||
break
|
|
||||||
node.childrenId.clear()
|
|
||||||
|
|
||||||
for childInfo in childrenInfo:
|
|
||||||
child : FsNode = None
|
|
||||||
id = childInfo["id"]
|
|
||||||
name = childInfo["name"]
|
|
||||||
fatherId = node.id
|
|
||||||
if id in self.nodes:
|
|
||||||
child = self.nodes[id]
|
|
||||||
else:
|
|
||||||
child = DirNode(id, name, fatherId) if childInfo["kind"].endswith("folder") else FileNode(id, name, fatherId)
|
|
||||||
self.nodes[id] = child
|
|
||||||
child.name = name
|
|
||||||
child.fatherId = fatherId
|
|
||||||
node.childrenId.append(id)
|
|
||||||
elif IsFile(node):
|
|
||||||
result = await self.client.get_download_url(node.id)
|
|
||||||
node.url = result["web_content_link"]
|
|
||||||
|
|
||||||
node.lastUpdate = datetime.now()
|
|
||||||
|
|
||||||
async def PathToNode(self, path : str) -> FsNode:
|
|
||||||
father, sonName = await self.PathToFatherNodeAndNodeName(path)
|
|
||||||
if sonName == "":
|
|
||||||
return father
|
|
||||||
if not IsDir(father):
|
|
||||||
return None
|
|
||||||
return self.FindChildInDirByName(father, sonName)
|
|
||||||
|
|
||||||
async def PathToFatherNodeAndNodeName(self, path : str) -> tuple[FsNode, str]:
|
|
||||||
pathWalker = PathWalker(path)
|
|
||||||
father : FsNode = None
|
|
||||||
sonName : str = None
|
|
||||||
current = self.root if pathWalker.IsAbsolute() else self.currentLocation
|
|
||||||
|
|
||||||
for spot in pathWalker.Walk():
|
|
||||||
if current is None:
|
|
||||||
father = None
|
|
||||||
break
|
|
||||||
if spot == "..":
|
|
||||||
current = self.GetFatherNode(current)
|
|
||||||
continue
|
|
||||||
father = current
|
|
||||||
if not IsDir(current):
|
|
||||||
current = None
|
|
||||||
continue
|
|
||||||
await self.Refresh(current)
|
|
||||||
if spot == ".":
|
|
||||||
continue
|
|
||||||
sonName = spot
|
|
||||||
current = self.FindChildInDirByName(current, spot)
|
|
||||||
|
|
||||||
if current != None:
|
|
||||||
father = self.GetFatherNode(current)
|
|
||||||
sonName = current.name
|
|
||||||
|
|
||||||
return father, sonName
|
|
||||||
|
|
||||||
def NodeToPath(self, node : FsNode, root : FsNode = None) -> str:
|
|
||||||
if root is None:
|
|
||||||
root = self.root
|
|
||||||
if node is root:
|
|
||||||
return "/"
|
|
||||||
spots : list[str] = []
|
|
||||||
current = node
|
|
||||||
while current is not root:
|
|
||||||
spots.append(current.name)
|
|
||||||
current = self.GetFatherNode(current)
|
|
||||||
spots.append("")
|
|
||||||
return "/".join(reversed(spots))
|
|
||||||
|
|
||||||
# commands #
|
|
||||||
async def Login(self, username : str = None, password : str = None) -> None:
|
|
||||||
if self.client != None and username is None and password is None:
|
|
||||||
username = self.client.username
|
|
||||||
password = self.client.password
|
|
||||||
|
|
||||||
if username == None and password == None:
|
|
||||||
raise Exception("Username and password are required")
|
|
||||||
|
|
||||||
self._init_client_by_username_and_password(username, password)
|
|
||||||
await self.client.login()
|
|
||||||
self._dump_login_info()
|
|
||||||
|
|
||||||
async def MakeDir(self, node : DirNode, name : str) -> DirNode:
|
|
||||||
result = await self.client.create_folder(name, node.id)
|
|
||||||
id = result["file"]["id"]
|
|
||||||
name = result["file"]["name"]
|
|
||||||
newDir = DirNode(id, name, node.id)
|
|
||||||
self.nodes[id] = newDir
|
|
||||||
node.childrenId.append(id)
|
|
||||||
return newDir
|
|
||||||
|
|
||||||
async def Download(self, url : str, dirNode : DirNode) -> PikPakTask :
|
|
||||||
task = PikPakTask(url, dirNode.id)
|
|
||||||
task.handler = self._pikpak_task_handler
|
|
||||||
self._add_task(task)
|
|
||||||
return task
|
|
||||||
|
|
||||||
async def Pull(self, node : FsNode) -> PikPakTask:
|
|
||||||
task = PikPakTask("", node.fatherId, node.id, PikPakTaskStatus.LOCAL_DOWNLOADING)
|
|
||||||
task.name = node.name
|
|
||||||
task.handler = self._pikpak_task_handler
|
|
||||||
self._add_task(task)
|
|
||||||
return task
|
|
||||||
|
|
||||||
async def QueryPikPakTasks(self, filterStatus : TaskStatus = None) -> list[PikPakTask]:
|
|
||||||
if PikPakTask.TAG not in self.taskQueues:
|
|
||||||
return []
|
|
||||||
taskQueue = self.taskQueues[PikPakTask.TAG]
|
|
||||||
if filterStatus is None:
|
|
||||||
return taskQueue
|
|
||||||
return [task for task in taskQueue if task._status == filterStatus]
|
|
||||||
|
|
||||||
async def QueryFileDownloadTasks(self, filterStatus : TaskStatus = None) -> list[FileDownloadTask]:
|
|
||||||
if FileDownloadTask.TAG not in self.taskQueues:
|
|
||||||
return []
|
|
||||||
taskQueue = self.taskQueues[FileDownloadTask.TAG]
|
|
||||||
if filterStatus is None:
|
|
||||||
return taskQueue
|
|
||||||
return [task for task in taskQueue if task._status == filterStatus]
|
|
||||||
|
|
||||||
async def Delete(self, nodes : list[FsNode]) -> None:
|
|
||||||
nodeIds = [node.id for node in nodes]
|
|
||||||
await self.client.delete_to_trash(nodeIds)
|
|
||||||
for node in nodes:
|
|
||||||
father = self.GetFatherNode(node)
|
|
||||||
father.childrenId.remove(node.id)
|
|
295
TaskManager.py
Normal file
295
TaskManager.py
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Awaitable, Callable, Dict
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import shortuuid
|
||||||
|
from PikPakFileSystem import PikPakFileSystem
|
||||||
|
from pikpakapi import DownloadStatus
|
||||||
|
import random
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
RUNNING = "running"
|
||||||
|
DONE = "done"
|
||||||
|
ERROR = "error"
|
||||||
|
PAUSED = "paused"
|
||||||
|
|
||||||
|
class TorrentTaskStatus(Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
REMOTE_DOWNLOADING = "remote"
|
||||||
|
LOCAL_DOWNLOADING = "local"
|
||||||
|
DONE = "done"
|
||||||
|
|
||||||
|
class FileDownloadTaskStatus(Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
DOWNLOADING = "downloading"
|
||||||
|
DONE = "done"
|
||||||
|
|
||||||
|
class TaskBase:
|
||||||
|
TAG = ""
|
||||||
|
MAX_CONCURRENT_NUMBER = 5
|
||||||
|
|
||||||
|
def __init__(self, client : PikPakFileSystem):
|
||||||
|
self.id : str = shortuuid.uuid()
|
||||||
|
self.status : TaskStatus = TaskStatus.PENDING
|
||||||
|
self.worker : asyncio.Task = None
|
||||||
|
self.handler : Callable[..., Awaitable] = None
|
||||||
|
self.client : PikPakFileSystem = client
|
||||||
|
|
||||||
|
def Resume(self):
|
||||||
|
if self.status in {TaskStatus.PAUSED, TaskStatus.ERROR}:
|
||||||
|
self.status = TaskStatus.PENDING
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentTask(TaskBase):
|
||||||
|
TAG = "TorrentTask"
|
||||||
|
MAX_CONCURRENT_NUMBER = 5
|
||||||
|
|
||||||
|
def __init__(self, torrent : str):
|
||||||
|
super().__init__(self)
|
||||||
|
self.torrent_status : TorrentTaskStatus = TorrentTaskStatus.PENDING
|
||||||
|
self.torrent : str = torrent
|
||||||
|
self.remote_full_path : str = None
|
||||||
|
self.remote_base_path : str = None
|
||||||
|
self.info : str = ""
|
||||||
|
|
||||||
|
# 和PikPak交互需要的信息
|
||||||
|
self.node_id : str = None
|
||||||
|
self.task_id : str = None
|
||||||
|
|
||||||
|
|
||||||
|
class FileDownloadTask(TaskBase):
|
||||||
|
TAG = "FileDownloadTask"
|
||||||
|
MAX_CONCURRENT_NUMBER = 5
|
||||||
|
|
||||||
|
def __init__(self, remote_path : str, owner_id : str):
|
||||||
|
super().__init__(self)
|
||||||
|
self.file_download_status : FileDownloadTaskStatus = FileDownloadTaskStatus.PENDING
|
||||||
|
self.remote_path : str = remote_path
|
||||||
|
self.owner_id : str = owner_id
|
||||||
|
|
||||||
|
async def TaskWorker(task : TaskBase):
|
||||||
|
try:
|
||||||
|
if task.status != TaskStatus.PENDING:
|
||||||
|
return
|
||||||
|
task.status = TaskStatus.RUNNING
|
||||||
|
await task.handler(task)
|
||||||
|
task.status = TaskStatus.DONE
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
task.status = TaskStatus.PAUSED
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"task failed, exception occurred: {e}")
|
||||||
|
task.status = TaskStatus.ERROR
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
#region 内部实现
|
||||||
|
def __init__(self, client : PikPakFileSystem):
|
||||||
|
self.taskQueues : Dict[str, list[TaskBase]] = {}
|
||||||
|
self.loop : asyncio.Task = None
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _loop(self):
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
for taskQueue in self.taskQueues.values():
|
||||||
|
notRunningTasks = [task for task in taskQueue if task.worker is None or task.worker.done()]
|
||||||
|
runningTasksNumber = len(taskQueue) - len(notRunningTasks)
|
||||||
|
for task in [task for task in notRunningTasks if task.status == TaskStatus.PENDING]:
|
||||||
|
if runningTasksNumber >= task.MAX_CONCURRENT_NUMBER:
|
||||||
|
break
|
||||||
|
task.worker = asyncio.create_task(TaskWorker(task))
|
||||||
|
runningTasksNumber += 1
|
||||||
|
|
||||||
|
async def _get_task_by_id(self, task_id : str) -> TaskBase:
|
||||||
|
for queue in self.taskQueues.values():
|
||||||
|
for task in queue:
|
||||||
|
if task.id == task_id:
|
||||||
|
return task
|
||||||
|
return None
|
||||||
|
|
||||||
|
#region 远程下载部分
|
||||||
|
|
||||||
|
async def _append_task(self, task : TaskBase):
|
||||||
|
queue = self.taskQueues.get(task.TAG, [])
|
||||||
|
queue.append(task)
|
||||||
|
self.taskQueues[task.TAG] = queue
|
||||||
|
|
||||||
|
async def _get_torrent_queue(self):
|
||||||
|
if TorrentTask.TAG not in self.taskQueues:
|
||||||
|
self.taskQueues[TorrentTask.TAG] = []
|
||||||
|
return self.taskQueues[TorrentTask.TAG]
|
||||||
|
|
||||||
|
async def _get_file_download_queue(self, owner_id : str):
|
||||||
|
if FileDownloadTask.TAG not in self.taskQueues:
|
||||||
|
self.taskQueues[FileDownloadTask.TAG] = []
|
||||||
|
queue = self.taskQueues[FileDownloadTask.TAG]
|
||||||
|
return [task for task in queue if task.owner_id == owner_id]
|
||||||
|
|
||||||
|
async def _on_torrent_task_pending(self, task : TorrentTask):
|
||||||
|
task.node_id, task.task_id = await self.client.RemoteDownload(task.torrent, task.remote_base_path)
|
||||||
|
task.torrent_status = TorrentTaskStatus.REMOTE_DOWNLOADING
|
||||||
|
|
||||||
|
async def _on_torrent_task_offline_downloading(self, task : TorrentTask):
|
||||||
|
wait_seconds = 3
|
||||||
|
while True:
|
||||||
|
status = await self.client.QueryTaskStatus(task.task_id, task.node_id)
|
||||||
|
if status in {DownloadStatus.not_found, DownloadStatus.not_downloading, DownloadStatus.error}:
|
||||||
|
task.torrent_status = TorrentTaskStatus.PENDING
|
||||||
|
raise Exception(f"remote download failed, status: {status}")
|
||||||
|
elif status == DownloadStatus.done:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(wait_seconds)
|
||||||
|
wait_seconds = wait_seconds * 1.5
|
||||||
|
|
||||||
|
task.remote_full_path = await self.client.UpdateDirectory(task.remote_base_path, task.node_id)
|
||||||
|
task.torrent_status = TorrentTaskStatus.LOCAL_DOWNLOADING
|
||||||
|
|
||||||
|
async def _on_torrent_local_downloading(self, task : TorrentTask):
|
||||||
|
path = task.remote_full_path
|
||||||
|
|
||||||
|
if not await self.client.IsDir(path):
|
||||||
|
await self._init_file_download_task(path, task.id)
|
||||||
|
else:
|
||||||
|
# 使用广度优先遍历
|
||||||
|
queue : list[str] = [path]
|
||||||
|
while len(queue) > 0:
|
||||||
|
current_path = queue.pop(0)
|
||||||
|
for child_name in await self.client.GetChildrenNames(current_path, False):
|
||||||
|
child_path = await self.client.JoinPath(current_path, child_name)
|
||||||
|
if await self.client.IsDir(child_path):
|
||||||
|
queue.append(child_path)
|
||||||
|
else:
|
||||||
|
await self._init_file_download_task(child_path, task.id)
|
||||||
|
|
||||||
|
# 开始等待下载任务完成
|
||||||
|
while True:
|
||||||
|
file_download_tasks = await self._get_file_download_queue(task.id)
|
||||||
|
all_number = len(file_download_tasks)
|
||||||
|
not_completed_number = 0
|
||||||
|
paused_number = 0
|
||||||
|
error_number = 0
|
||||||
|
for file_download_task in file_download_tasks:
|
||||||
|
if file_download_task.status == TaskStatus.PAUSED:
|
||||||
|
paused_number += 1
|
||||||
|
if file_download_task.status == TaskStatus.ERROR:
|
||||||
|
error_number += 1
|
||||||
|
if file_download_task.status in {TaskStatus.PENDING, TaskStatus.RUNNING}:
|
||||||
|
not_completed_number += 1
|
||||||
|
|
||||||
|
running_number = all_number - not_completed_number - paused_number - error_number
|
||||||
|
task.info = f"{running_number}/{all_number} ({paused_number}|{error_number})"
|
||||||
|
|
||||||
|
if not_completed_number > 0:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
continue
|
||||||
|
if error_number > 0:
|
||||||
|
raise Exception("file download failed")
|
||||||
|
if paused_number > 0:
|
||||||
|
raise asyncio.CancelledError()
|
||||||
|
break
|
||||||
|
|
||||||
|
task.torrent_status = TorrentTaskStatus.DONE
|
||||||
|
|
||||||
|
async def _on_torrent_task_cancelled(self, task : TorrentTask):
|
||||||
|
file_download_tasks = await self._get_file_download_queue(task.id)
|
||||||
|
for file_download_task in file_download_tasks:
|
||||||
|
if file_download_task.worker is not None:
|
||||||
|
file_download_task.worker.cancel()
|
||||||
|
|
||||||
|
async def _torrent_task_handler(self, task : TorrentTask):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if task.torrent_status == TorrentTaskStatus.PENDING:
|
||||||
|
await self._on_torrent_task_pending(task)
|
||||||
|
elif task.torrent_status == TorrentTaskStatus.REMOTE_DOWNLOADING:
|
||||||
|
await self._on_torrent_task_offline_downloading(task)
|
||||||
|
elif task.torrent_status == TorrentTaskStatus.LOCAL_DOWNLOADING:
|
||||||
|
await self._on_torrent_local_downloading(task)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
await self._on_torrent_task_cancelled(task)
|
||||||
|
raise
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region 文件下载部分
|
||||||
|
async def _init_file_download_task(self, remote_path : str, owner_id : str) -> str:
|
||||||
|
queue = await self._get_file_download_queue(owner_id)
|
||||||
|
for task in queue:
|
||||||
|
if not isinstance(task, FileDownloadTask):
|
||||||
|
continue
|
||||||
|
if task.remote_path == remote_path:
|
||||||
|
if task.status in {TaskStatus.PAUSED, TaskStatus.ERROR}:
|
||||||
|
task.status = TaskStatus.PENDING
|
||||||
|
return task.id
|
||||||
|
task = FileDownloadTask(remote_path, owner_id)
|
||||||
|
task.handler = self._file_download_task_handler
|
||||||
|
await self._append_task(task)
|
||||||
|
return task.id
|
||||||
|
|
||||||
|
async def _file_download_task_handler(self, task : FileDownloadTask):
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
if random.randint(1, 5) == 2:
|
||||||
|
raise asyncio.CancelledError()
|
||||||
|
if random.randint(1, 5) == 3:
|
||||||
|
raise Exception("random error")
|
||||||
|
pass
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 对外接口
|
||||||
|
|
||||||
|
def Start(self):
|
||||||
|
# todo: 从文件中恢复任务
|
||||||
|
if self.loop is None:
|
||||||
|
self.loop = asyncio.create_task(self._loop())
|
||||||
|
|
||||||
|
def Stop(self):
|
||||||
|
if self.loop is not None:
|
||||||
|
self.loop.cancel()
|
||||||
|
self.loop = None
|
||||||
|
# todo: 保存任务到文件
|
||||||
|
|
||||||
|
async def CreateTorrentTask(self, torrent : str, remote_base_path : str) -> str:
|
||||||
|
task = TorrentTask(torrent)
|
||||||
|
task.remote_base_path = remote_base_path
|
||||||
|
task.handler = self._torrent_task_handler
|
||||||
|
await self._append_task(task)
|
||||||
|
return task.id
|
||||||
|
|
||||||
|
async def PullRemote(self, remote_full_path : str) -> str:
|
||||||
|
if not await self.client.IfExists(remote_full_path):
|
||||||
|
raise Exception("target not found")
|
||||||
|
queue = await self._get_torrent_queue()
|
||||||
|
for task in queue:
|
||||||
|
if not isinstance(task, TorrentTask):
|
||||||
|
continue
|
||||||
|
if task.remote_full_path == remote_full_path:
|
||||||
|
return task.id
|
||||||
|
task = TorrentTask(None)
|
||||||
|
task.remote_full_path = remote_full_path
|
||||||
|
task.handler = self._torrent_task_handler
|
||||||
|
task.torrent_status = TorrentTaskStatus.LOCAL_DOWNLOADING
|
||||||
|
await self._append_task(task)
|
||||||
|
return task.id
|
||||||
|
|
||||||
|
async def QueryTasks(self, tag : str, filter_status : TaskStatus = None):
|
||||||
|
queue = self.taskQueues.get(tag, [])
|
||||||
|
if filter_status is None:
|
||||||
|
return queue
|
||||||
|
return [task for task in queue if task.status == filter_status]
|
||||||
|
|
||||||
|
async def StopTask(self, task_id : str):
|
||||||
|
task = await self._get_task_by_id(task_id)
|
||||||
|
if task is not None and task.worker is not None:
|
||||||
|
task.worker.cancel()
|
||||||
|
|
||||||
|
async def ResumeTask(self, task_id : str):
|
||||||
|
task = await self._get_task_by_id(task_id)
|
||||||
|
if task is not None:
|
||||||
|
task.Resume()
|
||||||
|
|
||||||
|
#endregion
|
162
main.py
162
main.py
@ -4,11 +4,11 @@ from functools import wraps
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import colorlog
|
import colorlog
|
||||||
from PikPakFs import PikPakFs, IsDir, IsFile, TaskStatus
|
from PikPakFileSystem import PikPakFileSystem
|
||||||
import os
|
import os
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
import wcwidth
|
|
||||||
import types
|
import types
|
||||||
|
from TaskManager import TaskManager, TaskStatus, TorrentTask, FileDownloadTask
|
||||||
|
|
||||||
LogFormatter = colorlog.ColoredFormatter(
|
LogFormatter = colorlog.ColoredFormatter(
|
||||||
"%(log_color)s%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
"%(log_color)s%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||||
@ -34,7 +34,7 @@ def setup_logging():
|
|||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
MainLoop : asyncio.AbstractEventLoop = None
|
MainLoop : asyncio.AbstractEventLoop = None
|
||||||
Client = PikPakFs("token.json", proxy="http://127.0.0.1:7897")
|
Client = PikPakFileSystem(auth_cache_path = "token.json", proxy_address="http://127.0.0.1:7897")
|
||||||
|
|
||||||
class RunSync:
|
class RunSync:
|
||||||
_current_task : asyncio.Task = None
|
_current_task : asyncio.Task = None
|
||||||
@ -69,18 +69,19 @@ class RunSync:
|
|||||||
else:
|
else:
|
||||||
return types.MethodType(self, instance)
|
return types.MethodType(self, instance)
|
||||||
|
|
||||||
class Console(cmd2.Cmd):
|
class App(cmd2.Cmd):
|
||||||
|
#region Console设置
|
||||||
def _io_worker(self, loop):
|
def _io_worker(self, loop):
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|
||||||
async def Input(self, prompt):
|
async def input(self, prompt):
|
||||||
async def _input(prompt):
|
async def _input(prompt):
|
||||||
return self._read_command_line(prompt)
|
return self._read_command_line(prompt)
|
||||||
future = asyncio.run_coroutine_threadsafe(_input(prompt), self.ioLoop)
|
future = asyncio.run_coroutine_threadsafe(_input(prompt), self.ioLoop)
|
||||||
return await asyncio.wrap_future(future)
|
return await asyncio.wrap_future(future)
|
||||||
|
|
||||||
async def Print(self, *args, **kwargs):
|
async def print(self, *args, **kwargs):
|
||||||
async def _print(*args, **kwargs):
|
async def _print(*args, **kwargs):
|
||||||
print(*args, **kwargs)
|
print(*args, **kwargs)
|
||||||
future = asyncio.run_coroutine_threadsafe(_print(*args, **kwargs), self.ioLoop)
|
future = asyncio.run_coroutine_threadsafe(_print(*args, **kwargs), self.ioLoop)
|
||||||
@ -93,6 +94,8 @@ class Console(cmd2.Cmd):
|
|||||||
self.log_handler.setLevel(logging.CRITICAL)
|
self.log_handler.setLevel(logging.CRITICAL)
|
||||||
logging.getLogger().addHandler(self.log_handler)
|
logging.getLogger().addHandler(self.log_handler)
|
||||||
|
|
||||||
|
self.task_manager = TaskManager(Client)
|
||||||
|
|
||||||
def preloop(self):
|
def preloop(self):
|
||||||
# 1. 设置忽略SIGINT
|
# 1. 设置忽略SIGINT
|
||||||
import signal
|
import signal
|
||||||
@ -109,6 +112,9 @@ class Console(cmd2.Cmd):
|
|||||||
self.saved_readline_settings = None
|
self.saved_readline_settings = None
|
||||||
with self.sigint_protection:
|
with self.sigint_protection:
|
||||||
self.saved_readline_settings = self._set_up_cmd2_readline()
|
self.saved_readline_settings = self._set_up_cmd2_readline()
|
||||||
|
|
||||||
|
# 4. 启动任务管理器
|
||||||
|
self.task_manager.Start()
|
||||||
|
|
||||||
def postloop(self):
|
def postloop(self):
|
||||||
# 1. 还原console设置
|
# 1. 还原console设置
|
||||||
@ -120,8 +126,13 @@ class Console(cmd2.Cmd):
|
|||||||
# https://stackoverflow.com/questions/51642267/asyncio-how-do-you-use-run-forever
|
# https://stackoverflow.com/questions/51642267/asyncio-how-do-you-use-run-forever
|
||||||
self.ioLoop.call_soon_threadsafe(self.ioLoop.stop)
|
self.ioLoop.call_soon_threadsafe(self.ioLoop.stop)
|
||||||
self.ioThread.join()
|
self.ioThread.join()
|
||||||
|
|
||||||
# commands #
|
# 3. 停止任务管理器
|
||||||
|
self.task_manager.Stop()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 所有命令
|
||||||
def do_logging_off(self, args):
|
def do_logging_off(self, args):
|
||||||
"""
|
"""
|
||||||
Disable logging
|
Disable logging
|
||||||
@ -153,27 +164,21 @@ class Console(cmd2.Cmd):
|
|||||||
Login to pikpak
|
Login to pikpak
|
||||||
"""
|
"""
|
||||||
await Client.Login(args.username, args.password)
|
await Client.Login(args.username, args.password)
|
||||||
await self.Print("Logged in successfully")
|
await self.print("Logged in successfully")
|
||||||
|
|
||||||
async def _path_completer(self, text, line, begidx, endidx, ignoreFiles):
|
async def _path_completer(self, text, line, begidx, endidx, ignoreFiles):
|
||||||
father, sonName = await Client.PathToFatherNodeAndNodeName(text)
|
father_path, son_name = await Client.SplitPath(text)
|
||||||
if not IsDir(father):
|
children_names = await Client.GetChildrenNames(father_path, ignoreFiles)
|
||||||
return []
|
|
||||||
matches = []
|
matches = []
|
||||||
matchesNode = []
|
for child_name in children_names:
|
||||||
for childId in father.childrenId:
|
if child_name.startswith(son_name):
|
||||||
child = Client.GetNodeById(childId)
|
self.display_matches.append(child_name)
|
||||||
if ignoreFiles and IsFile(child):
|
if son_name == "":
|
||||||
continue
|
matches.append(text + child_name)
|
||||||
if child.name.startswith(sonName):
|
elif text.endswith(son_name):
|
||||||
self.display_matches.append(child.name)
|
matches.append(text[:text.rfind(son_name)] + child_name)
|
||||||
if sonName == "":
|
if len(matches) == 1 and await Client.IsDir(father_path + matches[0]):
|
||||||
matches.append(text + child.name)
|
if matches[0] == son_name:
|
||||||
elif text.endswith(sonName):
|
|
||||||
matches.append(text[:text.rfind(sonName)] + child.name)
|
|
||||||
matchesNode.append(child)
|
|
||||||
if len(matchesNode) == 1 and IsDir(matchesNode[0]):
|
|
||||||
if matches[0] == sonName:
|
|
||||||
matches[0] += "/"
|
matches[0] += "/"
|
||||||
self.allow_appended_space = False
|
self.allow_appended_space = False
|
||||||
self.allow_closing_quote = False
|
self.allow_closing_quote = False
|
||||||
@ -184,49 +189,37 @@ class Console(cmd2.Cmd):
|
|||||||
return await self._path_completer(text, line, begidx, endidx, False)
|
return await self._path_completer(text, line, begidx, endidx, False)
|
||||||
|
|
||||||
ls_parser = cmd2.Cmd2ArgumentParser()
|
ls_parser = cmd2.Cmd2ArgumentParser()
|
||||||
ls_parser.add_argument("path", help="path", default="", nargs="?", type=RunSync(Client.PathToNode))
|
ls_parser.add_argument("path", help="path", default="", nargs="?")
|
||||||
@cmd2.with_argparser(ls_parser)
|
@cmd2.with_argparser(ls_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_ls(self, args):
|
async def do_ls(self, args):
|
||||||
"""
|
"""
|
||||||
List files in a directory
|
List files in a directory
|
||||||
"""
|
"""
|
||||||
node = args.path
|
if await Client.IsDir(args.path):
|
||||||
if node is None:
|
for child_name in await Client.GetChildrenNames(args.path, False):
|
||||||
await self.Print("Invalid path")
|
await self.print(child_name)
|
||||||
return
|
|
||||||
await Client.Refresh(node)
|
|
||||||
if IsDir(node):
|
|
||||||
for childId in node.childrenId:
|
|
||||||
child = Client.GetNodeById(childId)
|
|
||||||
await self.Print(child.name)
|
|
||||||
elif IsFile(node):
|
|
||||||
await self.Print(f"{node.name}: {node.url}")
|
|
||||||
|
|
||||||
@RunSync
|
@RunSync
|
||||||
async def complete_cd(self, text, line, begidx, endidx):
|
async def complete_cd(self, text, line, begidx, endidx):
|
||||||
return await self._path_completer(text, line, begidx, endidx, True)
|
return await self._path_completer(text, line, begidx, endidx, True)
|
||||||
|
|
||||||
cd_parser = cmd2.Cmd2ArgumentParser()
|
cd_parser = cmd2.Cmd2ArgumentParser()
|
||||||
cd_parser.add_argument("path", help="path", default="", nargs="?", type=RunSync(Client.PathToNode))
|
cd_parser.add_argument("path", help="path", default="", nargs="?")
|
||||||
@cmd2.with_argparser(cd_parser)
|
@cmd2.with_argparser(cd_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_cd(self, args):
|
async def do_cd(self, args):
|
||||||
"""
|
"""
|
||||||
Change directory
|
Change directory
|
||||||
"""
|
"""
|
||||||
node = args.path
|
await Client.SetCwd(args.path)
|
||||||
if not IsDir(node):
|
|
||||||
await self.Print("Invalid directory")
|
|
||||||
return
|
|
||||||
Client.currentLocation = node
|
|
||||||
|
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_cwd(self, args):
|
async def do_cwd(self, args):
|
||||||
"""
|
"""
|
||||||
Print current working directory
|
Print current working directory
|
||||||
"""
|
"""
|
||||||
await self.Print(Client.NodeToPath(Client.currentLocation))
|
await self.print(await Client.GetCwd())
|
||||||
|
|
||||||
def do_clear(self, args):
|
def do_clear(self, args):
|
||||||
"""
|
"""
|
||||||
@ -239,7 +232,7 @@ class Console(cmd2.Cmd):
|
|||||||
return await self._path_completer(text, line, begidx, endidx, False)
|
return await self._path_completer(text, line, begidx, endidx, False)
|
||||||
|
|
||||||
rm_parser = cmd2.Cmd2ArgumentParser()
|
rm_parser = cmd2.Cmd2ArgumentParser()
|
||||||
rm_parser.add_argument("paths", help="paths", default="", nargs="+", type=RunSync(Client.PathToNode))
|
rm_parser.add_argument("paths", help="paths", default="", nargs="+")
|
||||||
@cmd2.with_argparser(rm_parser)
|
@cmd2.with_argparser(rm_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_rm(self, args):
|
async def do_rm(self, args):
|
||||||
@ -253,56 +246,44 @@ class Console(cmd2.Cmd):
|
|||||||
return await self._path_completer(text, line, begidx, endidx, True)
|
return await self._path_completer(text, line, begidx, endidx, True)
|
||||||
|
|
||||||
mkdir_parser = cmd2.Cmd2ArgumentParser()
|
mkdir_parser = cmd2.Cmd2ArgumentParser()
|
||||||
mkdir_parser.add_argument("path_and_son", help="path and son", default="", nargs="?", type=RunSync(Client.PathToFatherNodeAndNodeName))
|
mkdir_parser.add_argument("path", help="new directory path")
|
||||||
@cmd2.with_argparser(mkdir_parser)
|
@cmd2.with_argparser(mkdir_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_mkdir(self, args):
|
async def do_mkdir(self, args):
|
||||||
"""
|
"""
|
||||||
Create a directory
|
Create a directory
|
||||||
"""
|
"""
|
||||||
father, sonName = args.path_and_son
|
await Client.MakeDir(args.path)
|
||||||
if not IsDir(father) or sonName == "" or sonName == None:
|
|
||||||
await self.Print("Invalid path")
|
|
||||||
return
|
|
||||||
child = Client.FindChildInDirByName(father, sonName)
|
|
||||||
if child is not None:
|
|
||||||
await self.Print("Path already exists")
|
|
||||||
return
|
|
||||||
await Client.MakeDir(father, sonName)
|
|
||||||
|
|
||||||
download_parser = cmd2.Cmd2ArgumentParser()
|
download_parser = cmd2.Cmd2ArgumentParser()
|
||||||
download_parser.add_argument("url", help="url")
|
download_parser.add_argument("torrent", help="torrent")
|
||||||
download_parser.add_argument("path", help="path", default="", nargs="?", type=RunSync(Client.PathToNode))
|
|
||||||
@cmd2.with_argparser(download_parser)
|
@cmd2.with_argparser(download_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_download(self, args):
|
async def do_download(self, args):
|
||||||
"""
|
"""
|
||||||
Download a file or directory
|
Download a file or directory
|
||||||
"""
|
"""
|
||||||
node = args.path
|
task_id = await self.task_manager.CreateTorrentTask(args.torrent, await Client.GetCwd())
|
||||||
if not IsDir(node):
|
await self.print(f"Task {task_id} created")
|
||||||
await self.Print("Invalid directory")
|
|
||||||
return
|
|
||||||
task = await Client.Download(args.url, node)
|
|
||||||
await self.Print(f"Task {task.id} created")
|
|
||||||
|
|
||||||
@RunSync
|
@RunSync
|
||||||
async def complete_pull(self, text, line, begidx, endidx):
|
async def complete_pull(self, text, line, begidx, endidx):
|
||||||
return await self._path_completer(text, line, begidx, endidx, False)
|
return await self._path_completer(text, line, begidx, endidx, False)
|
||||||
|
|
||||||
pull_parser = cmd2.Cmd2ArgumentParser()
|
pull_parser = cmd2.Cmd2ArgumentParser()
|
||||||
pull_parser.add_argument("target", help="pull target", type=RunSync(Client.PathToNode))
|
pull_parser.add_argument("target", help="pull target")
|
||||||
@cmd2.with_argparser(pull_parser)
|
@cmd2.with_argparser(pull_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
async def do_pull(self, args):
|
async def do_pull(self, args):
|
||||||
"""
|
"""
|
||||||
Pull a file or directory
|
Pull a file or directory
|
||||||
"""
|
"""
|
||||||
await Client.Pull(args.target)
|
task_id = await self.task_manager.PullRemote(await Client.ToFullPath(args.target))
|
||||||
|
await self.print(f"Task {task_id} created")
|
||||||
|
|
||||||
|
|
||||||
query_parser = cmd2.Cmd2ArgumentParser()
|
query_parser = cmd2.Cmd2ArgumentParser()
|
||||||
query_parser.add_argument("-t", "--type", help="type", nargs="?", choices=["pikpak", "filedownload"], default="pikpak")
|
query_parser.add_argument("-t", "--type", help="type", nargs="?", choices=["torrent", "file"], default="torrent")
|
||||||
query_parser.add_argument("-f", "--filter", help="filter", nargs="?", choices=[member.value for member in TaskStatus])
|
query_parser.add_argument("-f", "--filter", help="filter", nargs="?", choices=[member.value for member in TaskStatus])
|
||||||
@cmd2.with_argparser(query_parser)
|
@cmd2.with_argparser(query_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
@ -310,21 +291,21 @@ class Console(cmd2.Cmd):
|
|||||||
"""
|
"""
|
||||||
Query All Tasks
|
Query All Tasks
|
||||||
"""
|
"""
|
||||||
if args.type == "pikpak":
|
filter_status = TaskStatus(args.filter) if args.filter is not None else None
|
||||||
tasks = await Client.QueryPikPakTasks(TaskStatus(args.filter) if args.filter is not None else None)
|
if args.type == "torrent":
|
||||||
|
tasks = await self.task_manager.QueryTasks(TorrentTask.TAG, filter_status)
|
||||||
# 格式化输出所有task信息id,status,lastStatus的信息,输出表格
|
# 格式化输出所有task信息id,status,lastStatus的信息,输出表格
|
||||||
table = [[task.id, task._status.value, task.status.value, task.progress] for task in tasks]
|
table = [[task.id, task.status.value, task.torrent_status.value, task.info] for task in tasks if isinstance(task, TorrentTask)]
|
||||||
headers = ["id", "status", "details", "progress"]
|
headers = ["id", "status", "details", "progress"]
|
||||||
await self.Print(tabulate(table, headers, tablefmt="grid"))
|
await self.print(tabulate(table, headers, tablefmt="grid"))
|
||||||
elif args.type == "filedownload":
|
elif args.type == "file":
|
||||||
tasks = await Client.QueryFileDownloadTasks(TaskStatus(args.filter) if args.filter is not None else None)
|
tasks = await self.task_manager.QueryTasks(FileDownloadTask.TAG, filter_status)
|
||||||
# 格式化输出所有task信息id,status,lastStatus的信息,输出表格
|
table = [[task.id, task.status.value, task.file_download_status, task.remote_path] for task in tasks if isinstance(task, FileDownloadTask)]
|
||||||
table = [[task.id, task._status.value, task.status.value, task.relativePath] for task in tasks]
|
headers = ["id", "status", "details", "remote_path"]
|
||||||
headers = ["id", "status", "details", "path"]
|
await self.print(tabulate(table, headers, tablefmt="grid"))
|
||||||
await self.Print(tabulate(table, headers, tablefmt="grid"))
|
|
||||||
|
|
||||||
taskid_parser = cmd2.Cmd2ArgumentParser()
|
taskid_parser = cmd2.Cmd2ArgumentParser()
|
||||||
taskid_parser.add_argument("taskId", help="taskId")
|
taskid_parser.add_argument("task_id", help="task id")
|
||||||
|
|
||||||
@cmd2.with_argparser(taskid_parser)
|
@cmd2.with_argparser(taskid_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
@ -332,7 +313,7 @@ class Console(cmd2.Cmd):
|
|||||||
"""
|
"""
|
||||||
Stop a task
|
Stop a task
|
||||||
"""
|
"""
|
||||||
await Client.StopTask(args.taskId)
|
await self.task_manager.StopTask(args.task_id)
|
||||||
|
|
||||||
@cmd2.with_argparser(taskid_parser)
|
@cmd2.with_argparser(taskid_parser)
|
||||||
@RunSync
|
@RunSync
|
||||||
@ -340,27 +321,30 @@ class Console(cmd2.Cmd):
|
|||||||
"""
|
"""
|
||||||
Resume a task
|
Resume a task
|
||||||
"""
|
"""
|
||||||
await Client.ResumeTask(args.taskId)
|
await self.task_manager.ResumeTask(args.task_id)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region APP入口
|
||||||
async def mainLoop():
|
async def mainLoop():
|
||||||
global MainLoop, Client
|
global MainLoop
|
||||||
MainLoop = asyncio.get_running_loop()
|
MainLoop = asyncio.get_running_loop()
|
||||||
clientWorker = Client.Start()
|
app = App()
|
||||||
|
|
||||||
console = Console()
|
app.preloop()
|
||||||
console.preloop()
|
|
||||||
try:
|
try:
|
||||||
stop = False
|
stop = False
|
||||||
while not stop:
|
while not stop:
|
||||||
line = await console.Input(console.prompt)
|
line = await app.input(app.prompt)
|
||||||
try:
|
try:
|
||||||
stop = console.onecmd_plus_hooks(line)
|
stop = app.onecmd_plus_hooks(line)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
await console.Print("^C: Task cancelled")
|
await app.print("^C: Task cancelled")
|
||||||
finally:
|
finally:
|
||||||
console.postloop()
|
app.postloop()
|
||||||
clientWorker.cancel()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
nest_asyncio.apply()
|
nest_asyncio.apply()
|
||||||
asyncio.run(mainLoop())
|
asyncio.run(mainLoop())
|
||||||
|
#endregion
|
16
utils.py
16
utils.py
@ -1,16 +0,0 @@
|
|||||||
class PathWalker():
|
|
||||||
def __init__(self, path : str, sep : str = "/"):
|
|
||||||
self._path_spots : list[str] = []
|
|
||||||
if not path.startswith(sep):
|
|
||||||
self._path_spots.append(".")
|
|
||||||
path_spots : list[str] = path.split(sep)
|
|
||||||
self._path_spots.extend(path_spots)
|
|
||||||
|
|
||||||
def IsAbsolute(self) -> bool:
|
|
||||||
return len(self._path_spots) == 0 or self._path_spots[0] != "."
|
|
||||||
|
|
||||||
def AppendSpot(self, spot):
|
|
||||||
self._path_spots.append(spot)
|
|
||||||
|
|
||||||
def Walk(self) -> list[str]:
|
|
||||||
return self._path_spots
|
|
Loading…
x
Reference in New Issue
Block a user