分模块
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 threading
|
||||
import colorlog
|
||||
from PikPakFs import PikPakFs, IsDir, IsFile, TaskStatus
|
||||
from PikPakFileSystem import PikPakFileSystem
|
||||
import os
|
||||
from tabulate import tabulate
|
||||
import wcwidth
|
||||
import types
|
||||
from TaskManager import TaskManager, TaskStatus, TorrentTask, FileDownloadTask
|
||||
|
||||
LogFormatter = colorlog.ColoredFormatter(
|
||||
"%(log_color)s%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||
@ -34,7 +34,7 @@ def setup_logging():
|
||||
|
||||
setup_logging()
|
||||
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:
|
||||
_current_task : asyncio.Task = None
|
||||
@ -69,18 +69,19 @@ class RunSync:
|
||||
else:
|
||||
return types.MethodType(self, instance)
|
||||
|
||||
class Console(cmd2.Cmd):
|
||||
class App(cmd2.Cmd):
|
||||
#region Console设置
|
||||
def _io_worker(self, loop):
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
|
||||
async def Input(self, prompt):
|
||||
async def input(self, prompt):
|
||||
async def _input(prompt):
|
||||
return self._read_command_line(prompt)
|
||||
future = asyncio.run_coroutine_threadsafe(_input(prompt), self.ioLoop)
|
||||
return await asyncio.wrap_future(future)
|
||||
|
||||
async def Print(self, *args, **kwargs):
|
||||
async def print(self, *args, **kwargs):
|
||||
async def _print(*args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
future = asyncio.run_coroutine_threadsafe(_print(*args, **kwargs), self.ioLoop)
|
||||
@ -93,6 +94,8 @@ class Console(cmd2.Cmd):
|
||||
self.log_handler.setLevel(logging.CRITICAL)
|
||||
logging.getLogger().addHandler(self.log_handler)
|
||||
|
||||
self.task_manager = TaskManager(Client)
|
||||
|
||||
def preloop(self):
|
||||
# 1. 设置忽略SIGINT
|
||||
import signal
|
||||
@ -109,6 +112,9 @@ class Console(cmd2.Cmd):
|
||||
self.saved_readline_settings = None
|
||||
with self.sigint_protection:
|
||||
self.saved_readline_settings = self._set_up_cmd2_readline()
|
||||
|
||||
# 4. 启动任务管理器
|
||||
self.task_manager.Start()
|
||||
|
||||
def postloop(self):
|
||||
# 1. 还原console设置
|
||||
@ -120,8 +126,13 @@ class Console(cmd2.Cmd):
|
||||
# https://stackoverflow.com/questions/51642267/asyncio-how-do-you-use-run-forever
|
||||
self.ioLoop.call_soon_threadsafe(self.ioLoop.stop)
|
||||
self.ioThread.join()
|
||||
|
||||
# commands #
|
||||
|
||||
# 3. 停止任务管理器
|
||||
self.task_manager.Stop()
|
||||
|
||||
#endregion
|
||||
|
||||
#region 所有命令
|
||||
def do_logging_off(self, args):
|
||||
"""
|
||||
Disable logging
|
||||
@ -153,27 +164,21 @@ class Console(cmd2.Cmd):
|
||||
Login to pikpak
|
||||
"""
|
||||
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):
|
||||
father, sonName = await Client.PathToFatherNodeAndNodeName(text)
|
||||
if not IsDir(father):
|
||||
return []
|
||||
father_path, son_name = await Client.SplitPath(text)
|
||||
children_names = await Client.GetChildrenNames(father_path, ignoreFiles)
|
||||
matches = []
|
||||
matchesNode = []
|
||||
for childId in father.childrenId:
|
||||
child = Client.GetNodeById(childId)
|
||||
if ignoreFiles and IsFile(child):
|
||||
continue
|
||||
if child.name.startswith(sonName):
|
||||
self.display_matches.append(child.name)
|
||||
if sonName == "":
|
||||
matches.append(text + child.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:
|
||||
for child_name in children_names:
|
||||
if child_name.startswith(son_name):
|
||||
self.display_matches.append(child_name)
|
||||
if son_name == "":
|
||||
matches.append(text + child_name)
|
||||
elif text.endswith(son_name):
|
||||
matches.append(text[:text.rfind(son_name)] + child_name)
|
||||
if len(matches) == 1 and await Client.IsDir(father_path + matches[0]):
|
||||
if matches[0] == son_name:
|
||||
matches[0] += "/"
|
||||
self.allow_appended_space = False
|
||||
self.allow_closing_quote = False
|
||||
@ -184,49 +189,37 @@ class Console(cmd2.Cmd):
|
||||
return await self._path_completer(text, line, begidx, endidx, False)
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
async def do_ls(self, args):
|
||||
"""
|
||||
List files in a directory
|
||||
"""
|
||||
node = args.path
|
||||
if node is None:
|
||||
await self.Print("Invalid path")
|
||||
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}")
|
||||
if await Client.IsDir(args.path):
|
||||
for child_name in await Client.GetChildrenNames(args.path, False):
|
||||
await self.print(child_name)
|
||||
|
||||
@RunSync
|
||||
async def complete_cd(self, text, line, begidx, endidx):
|
||||
return await self._path_completer(text, line, begidx, endidx, True)
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
async def do_cd(self, args):
|
||||
"""
|
||||
Change directory
|
||||
"""
|
||||
node = args.path
|
||||
if not IsDir(node):
|
||||
await self.Print("Invalid directory")
|
||||
return
|
||||
Client.currentLocation = node
|
||||
await Client.SetCwd(args.path)
|
||||
|
||||
@RunSync
|
||||
async def do_cwd(self, args):
|
||||
"""
|
||||
Print current working directory
|
||||
"""
|
||||
await self.Print(Client.NodeToPath(Client.currentLocation))
|
||||
await self.print(await Client.GetCwd())
|
||||
|
||||
def do_clear(self, args):
|
||||
"""
|
||||
@ -239,7 +232,7 @@ class Console(cmd2.Cmd):
|
||||
return await self._path_completer(text, line, begidx, endidx, False)
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
async def do_rm(self, args):
|
||||
@ -253,56 +246,44 @@ class Console(cmd2.Cmd):
|
||||
return await self._path_completer(text, line, begidx, endidx, True)
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
async def do_mkdir(self, args):
|
||||
"""
|
||||
Create a directory
|
||||
"""
|
||||
father, sonName = args.path_and_son
|
||||
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)
|
||||
await Client.MakeDir(args.path)
|
||||
|
||||
download_parser = cmd2.Cmd2ArgumentParser()
|
||||
download_parser.add_argument("url", help="url")
|
||||
download_parser.add_argument("path", help="path", default="", nargs="?", type=RunSync(Client.PathToNode))
|
||||
download_parser.add_argument("torrent", help="torrent")
|
||||
@cmd2.with_argparser(download_parser)
|
||||
@RunSync
|
||||
async def do_download(self, args):
|
||||
"""
|
||||
Download a file or directory
|
||||
"""
|
||||
node = args.path
|
||||
if not IsDir(node):
|
||||
await self.Print("Invalid directory")
|
||||
return
|
||||
task = await Client.Download(args.url, node)
|
||||
await self.Print(f"Task {task.id} created")
|
||||
task_id = await self.task_manager.CreateTorrentTask(args.torrent, await Client.GetCwd())
|
||||
await self.print(f"Task {task_id} created")
|
||||
|
||||
@RunSync
|
||||
async def complete_pull(self, text, line, begidx, endidx):
|
||||
return await self._path_completer(text, line, begidx, endidx, False)
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
async def do_pull(self, args):
|
||||
"""
|
||||
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.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])
|
||||
@cmd2.with_argparser(query_parser)
|
||||
@RunSync
|
||||
@ -310,21 +291,21 @@ class Console(cmd2.Cmd):
|
||||
"""
|
||||
Query All Tasks
|
||||
"""
|
||||
if args.type == "pikpak":
|
||||
tasks = await Client.QueryPikPakTasks(TaskStatus(args.filter) if args.filter is not None else None)
|
||||
filter_status = 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的信息,输出表格
|
||||
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"]
|
||||
await self.Print(tabulate(table, headers, tablefmt="grid"))
|
||||
elif args.type == "filedownload":
|
||||
tasks = await Client.QueryFileDownloadTasks(TaskStatus(args.filter) if args.filter is not None else None)
|
||||
# 格式化输出所有task信息id,status,lastStatus的信息,输出表格
|
||||
table = [[task.id, task._status.value, task.status.value, task.relativePath] for task in tasks]
|
||||
headers = ["id", "status", "details", "path"]
|
||||
await self.Print(tabulate(table, headers, tablefmt="grid"))
|
||||
await self.print(tabulate(table, headers, tablefmt="grid"))
|
||||
elif args.type == "file":
|
||||
tasks = await self.task_manager.QueryTasks(FileDownloadTask.TAG, filter_status)
|
||||
table = [[task.id, task.status.value, task.file_download_status, task.remote_path] for task in tasks if isinstance(task, FileDownloadTask)]
|
||||
headers = ["id", "status", "details", "remote_path"]
|
||||
await self.print(tabulate(table, headers, tablefmt="grid"))
|
||||
|
||||
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)
|
||||
@RunSync
|
||||
@ -332,7 +313,7 @@ class Console(cmd2.Cmd):
|
||||
"""
|
||||
Stop a task
|
||||
"""
|
||||
await Client.StopTask(args.taskId)
|
||||
await self.task_manager.StopTask(args.task_id)
|
||||
|
||||
@cmd2.with_argparser(taskid_parser)
|
||||
@RunSync
|
||||
@ -340,27 +321,30 @@ class Console(cmd2.Cmd):
|
||||
"""
|
||||
Resume a task
|
||||
"""
|
||||
await Client.ResumeTask(args.taskId)
|
||||
await self.task_manager.ResumeTask(args.task_id)
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region APP入口
|
||||
async def mainLoop():
|
||||
global MainLoop, Client
|
||||
global MainLoop
|
||||
MainLoop = asyncio.get_running_loop()
|
||||
clientWorker = Client.Start()
|
||||
app = App()
|
||||
|
||||
console = Console()
|
||||
console.preloop()
|
||||
app.preloop()
|
||||
try:
|
||||
stop = False
|
||||
while not stop:
|
||||
line = await console.Input(console.prompt)
|
||||
line = await app.input(app.prompt)
|
||||
try:
|
||||
stop = console.onecmd_plus_hooks(line)
|
||||
stop = app.onecmd_plus_hooks(line)
|
||||
except asyncio.CancelledError:
|
||||
await console.Print("^C: Task cancelled")
|
||||
await app.print("^C: Task cancelled")
|
||||
finally:
|
||||
console.postloop()
|
||||
clientWorker.cancel()
|
||||
app.postloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
nest_asyncio.apply()
|
||||
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