Upload 6 files
Browse files- main.py +130 -0
- pikpakapi/PikpakException.py +7 -0
- pikpakapi/__init__.py +1062 -0
- pikpakapi/enums.py +9 -0
- pikpakapi/utils.py +108 -0
- templates/index.html +1929 -0
main.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import asyncio
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
|
6 |
+
import httpx
|
7 |
+
|
8 |
+
from pikpakapi import PikPakApi
|
9 |
+
|
10 |
+
from typing import Union
|
11 |
+
from fastapi import (
|
12 |
+
FastAPI,
|
13 |
+
Depends,
|
14 |
+
Request,
|
15 |
+
Body,
|
16 |
+
Response,
|
17 |
+
HTTPException,
|
18 |
+
status,
|
19 |
+
Request,
|
20 |
+
)
|
21 |
+
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
|
22 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
23 |
+
from fastapi.templating import Jinja2Templates
|
24 |
+
from fastapi.middleware.cors import CORSMiddleware
|
25 |
+
from pydantic import BaseModel, Extra
|
26 |
+
|
27 |
+
|
28 |
+
class PostRequest(BaseModel):
|
29 |
+
class Config:
|
30 |
+
extra = Extra.allow
|
31 |
+
|
32 |
+
|
33 |
+
security = HTTPBearer()
|
34 |
+
# SECRET_TOKEN = "SECRET_TOKEN"
|
35 |
+
SECRET_TOKEN = os.getenv("SECRET_TOKEN")
|
36 |
+
if SECRET_TOKEN is None:
|
37 |
+
raise ValueError("请在环境变量中设置SECRET_TOKEN,确保安全!")
|
38 |
+
|
39 |
+
THUNDERX_USERNAME = os.getenv("THUNDERX_USERNAME")
|
40 |
+
if THUNDERX_USERNAME is None:
|
41 |
+
raise ValueError("请在环境变量中设置THUNDERX_USERNAME,用户名【邮箱】用来登陆!")
|
42 |
+
|
43 |
+
|
44 |
+
THUNDERX_PASSWORD = os.getenv("THUNDERX_PASSWORD")
|
45 |
+
if THUNDERX_PASSWORD is None:
|
46 |
+
raise ValueError("请在环境变量中设置THUNDERX_PASSWORD,密码用来登陆!")
|
47 |
+
|
48 |
+
PROXY_URL = os.getenv("PROXY_URL")
|
49 |
+
|
50 |
+
|
51 |
+
async def verify_token(
|
52 |
+
request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)
|
53 |
+
):
|
54 |
+
# 验证Bearer格式
|
55 |
+
if credentials.scheme != "Bearer":
|
56 |
+
raise HTTPException(
|
57 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
58 |
+
detail="Invalid authentication scheme",
|
59 |
+
)
|
60 |
+
|
61 |
+
# 验证令牌内容
|
62 |
+
if credentials.credentials != SECRET_TOKEN:
|
63 |
+
raise HTTPException(
|
64 |
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
|
65 |
+
)
|
66 |
+
|
67 |
+
|
68 |
+
app = FastAPI(dependencies=[Depends(verify_token)])
|
69 |
+
app.add_middleware(
|
70 |
+
CORSMiddleware,
|
71 |
+
allow_origins=["*"],
|
72 |
+
allow_credentials=True,
|
73 |
+
allow_methods=["*"],
|
74 |
+
allow_headers=["*"],
|
75 |
+
)
|
76 |
+
|
77 |
+
templates = Jinja2Templates(
|
78 |
+
directory="templates", variable_start_string="{[", variable_end_string="]}"
|
79 |
+
)
|
80 |
+
|
81 |
+
THUNDERX_CLIENT = None
|
82 |
+
|
83 |
+
|
84 |
+
async def log_token(THUNDERX_CLIENT, extra_data):
|
85 |
+
logging.info(f"Token: {THUNDERX_CLIENT.encoded_token}, Extra Data: {extra_data}")
|
86 |
+
|
87 |
+
|
88 |
+
@app.on_event("startup")
|
89 |
+
async def init_client():
|
90 |
+
global THUNDERX_CLIENT
|
91 |
+
if not os.path.exists("thunderx.txt"):
|
92 |
+
THUNDERX_CLIENT = PikPakApi(
|
93 |
+
username=THUNDERX_USERNAME,
|
94 |
+
password=THUNDERX_PASSWORD,
|
95 |
+
httpx_client_args=None,
|
96 |
+
token_refresh_callback=log_token,
|
97 |
+
token_refresh_callback_kwargs={"extra_data": "test"},
|
98 |
+
)
|
99 |
+
await THUNDERX_CLIENT.login()
|
100 |
+
await THUNDERX_CLIENT.refresh_access_token()
|
101 |
+
with open("thunderx.json", "w") as f:
|
102 |
+
f.write(json.dumps(THUNDERX_CLIENT.to_dict(), indent=4))
|
103 |
+
else:
|
104 |
+
with open("thunderx.txt", "r") as f:
|
105 |
+
data = json.load(f)
|
106 |
+
THUNDERX_CLIENT = PikPakApi.from_dict(data)
|
107 |
+
# await client.refresh_access_token()
|
108 |
+
print(json.dumps(THUNDERX_CLIENT.get_user_info(), indent=4))
|
109 |
+
|
110 |
+
print(
|
111 |
+
json.dumps(
|
112 |
+
await THUNDERX_CLIENT.events(),
|
113 |
+
indent=4,
|
114 |
+
)
|
115 |
+
)
|
116 |
+
|
117 |
+
|
118 |
+
@app.get("/", response_class=HTMLResponse)
|
119 |
+
async def home(request: Request):
|
120 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
121 |
+
|
122 |
+
|
123 |
+
@app.get("/files")
|
124 |
+
async def get_files(kw: str = ""):
|
125 |
+
return await THUNDERX_CLIENT.file_list()
|
126 |
+
|
127 |
+
|
128 |
+
@app.get("/userinfo")
|
129 |
+
async def userinfo():
|
130 |
+
return THUNDERX_CLIENT.get_user_info()
|
pikpakapi/PikpakException.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class PikpakException(Exception):
|
2 |
+
def __init__(self, message):
|
3 |
+
super().__init__(message)
|
4 |
+
|
5 |
+
|
6 |
+
class PikpakRetryException(PikpakException):
|
7 |
+
pass
|
pikpakapi/__init__.py
ADDED
@@ -0,0 +1,1062 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import binascii
|
3 |
+
import inspect
|
4 |
+
import json
|
5 |
+
import logging
|
6 |
+
import re
|
7 |
+
from base64 import b64decode, b64encode
|
8 |
+
from hashlib import md5
|
9 |
+
from types import NoneType
|
10 |
+
from typing import Any, Dict, List, Optional, Callable, Coroutine
|
11 |
+
from urllib.parse import urljoin
|
12 |
+
|
13 |
+
import httpx
|
14 |
+
|
15 |
+
from .PikpakException import PikpakException, PikpakRetryException
|
16 |
+
from .enums import DownloadStatus
|
17 |
+
from .utils import (
|
18 |
+
CLIENT_ID,
|
19 |
+
CLIENT_SECRET,
|
20 |
+
CLIENT_VERSION,
|
21 |
+
PACKAG_ENAME,
|
22 |
+
build_custom_user_agent,
|
23 |
+
captcha_sign,
|
24 |
+
get_timestamp,
|
25 |
+
)
|
26 |
+
|
27 |
+
|
28 |
+
class PikPakApi:
|
29 |
+
"""
|
30 |
+
PikPakApi class
|
31 |
+
|
32 |
+
Attributes:
|
33 |
+
PIKPAK_API_HOST: str - PikPak API host
|
34 |
+
PIKPAK_USER_HOST: str - PikPak user API host
|
35 |
+
|
36 |
+
username: str - username of the user
|
37 |
+
password: str - password of the user
|
38 |
+
encoded_token: str - encoded token of the user with access and refresh tokens
|
39 |
+
access_token: str - access token of the user , expire in 7200
|
40 |
+
refresh_token: str - refresh token of the user
|
41 |
+
user_id: str - user id of the user
|
42 |
+
token_refresh_callback: Callable[[PikPakApi, **Any], Coroutine[Any, Any, None]] - async callback function to be called after token refresh
|
43 |
+
token_refresh_callback_kwargs: Dict[str, Any] - custom arguments to be passed to the token refresh callback
|
44 |
+
"""
|
45 |
+
|
46 |
+
PIKPAK_API_HOST = "api-pan.xunleix.com"
|
47 |
+
PIKPAK_USER_HOST = "xluser-ssl.xunleix.com"
|
48 |
+
|
49 |
+
def __init__(
|
50 |
+
self,
|
51 |
+
username: Optional[str] = None,
|
52 |
+
password: Optional[str] = None,
|
53 |
+
encoded_token: Optional[str] = None,
|
54 |
+
httpx_client_args: Optional[Dict[str, Any]] = None,
|
55 |
+
device_id: Optional[str] = None,
|
56 |
+
request_max_retries: int = 3,
|
57 |
+
request_initial_backoff: float = 3.0,
|
58 |
+
token_refresh_callback: Optional[Callable] = None,
|
59 |
+
token_refresh_callback_kwargs: Optional[Dict[str, Any]] = None,
|
60 |
+
):
|
61 |
+
"""
|
62 |
+
username: str - username of the user
|
63 |
+
password: str - password of the user
|
64 |
+
encoded_token: str - encoded token of the user with access and refresh token
|
65 |
+
httpx_client_args: dict - extra arguments for httpx.AsyncClient (https://www.python-httpx.org/api/#asyncclient)
|
66 |
+
device_id: str - device id to identify the device
|
67 |
+
request_max_retries: int - maximum number of retries for requests
|
68 |
+
request_initial_backoff: float - initial backoff time for retries
|
69 |
+
token_refresh_callback: Callable[[PikPakApi, **Any], Coroutine[Any, Any, None]] - async callback function to be called after token refresh
|
70 |
+
token_refresh_callback_kwargs: Dict[str, Any] - custom arguments to be passed to the token refresh callback
|
71 |
+
"""
|
72 |
+
|
73 |
+
self.username = username
|
74 |
+
self.password = password
|
75 |
+
self.encoded_token = encoded_token
|
76 |
+
self.max_retries = request_max_retries
|
77 |
+
self.initial_backoff = request_initial_backoff
|
78 |
+
self.token_refresh_callback = token_refresh_callback
|
79 |
+
self.token_refresh_callback_kwargs = token_refresh_callback_kwargs or {}
|
80 |
+
|
81 |
+
self.access_token = None
|
82 |
+
self.refresh_token = None
|
83 |
+
self.user_id = None
|
84 |
+
|
85 |
+
self.data_response = None
|
86 |
+
|
87 |
+
# device_id is used to identify the device, if not provided, a random device_id will be generated, 32 characters
|
88 |
+
self.device_id = (
|
89 |
+
device_id
|
90 |
+
if device_id
|
91 |
+
else md5(f"{self.username}{self.password}".encode()).hexdigest()
|
92 |
+
)
|
93 |
+
self.captcha_token = None
|
94 |
+
|
95 |
+
httpx_client_args = httpx_client_args or {"timeout": 10}
|
96 |
+
self.httpx_client = httpx.AsyncClient(**httpx_client_args)
|
97 |
+
|
98 |
+
self._path_id_cache: Dict[str, Any] = {}
|
99 |
+
|
100 |
+
self.user_agent: Optional[str] = None
|
101 |
+
|
102 |
+
if self.encoded_token:
|
103 |
+
self.decode_token()
|
104 |
+
elif self.username and self.password:
|
105 |
+
pass
|
106 |
+
else:
|
107 |
+
raise PikpakException("username and password or encoded_token is required")
|
108 |
+
|
109 |
+
@classmethod
|
110 |
+
def from_dict(cls, data: Dict[str, Any]) -> "PikPakApi":
|
111 |
+
"""
|
112 |
+
Create PikPakApi object from a dictionary
|
113 |
+
"""
|
114 |
+
params = inspect.signature(cls).parameters
|
115 |
+
filtered_data = {key: data[key] for key in params if key in data}
|
116 |
+
client = cls(
|
117 |
+
**filtered_data,
|
118 |
+
)
|
119 |
+
client.__dict__.update(data)
|
120 |
+
return client
|
121 |
+
|
122 |
+
def to_dict(self) -> Dict[str, Any]:
|
123 |
+
"""
|
124 |
+
Returns the PikPakApi object as a dictionary
|
125 |
+
"""
|
126 |
+
data = self.__dict__.copy()
|
127 |
+
# remove can't be serialized attributes
|
128 |
+
keys_to_delete = [
|
129 |
+
k
|
130 |
+
for k, v in data.items()
|
131 |
+
if not type(v) in [str, int, float, bool, list, dict, NoneType]
|
132 |
+
]
|
133 |
+
for k in keys_to_delete:
|
134 |
+
del data[k]
|
135 |
+
return data
|
136 |
+
|
137 |
+
def build_custom_user_agent(self) -> str:
|
138 |
+
|
139 |
+
self.user_agent = build_custom_user_agent(
|
140 |
+
device_id=self.device_id,
|
141 |
+
user_id=self.user_id if self.user_id else "",
|
142 |
+
)
|
143 |
+
return self.user_agent
|
144 |
+
|
145 |
+
def get_headers(self, access_token: Optional[str] = None) -> Dict[str, str]:
|
146 |
+
"""
|
147 |
+
Returns the headers to use for the requests.
|
148 |
+
"""
|
149 |
+
headers = {
|
150 |
+
"User-Agent": (
|
151 |
+
self.build_custom_user_agent()
|
152 |
+
if self.captcha_token
|
153 |
+
else "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
154 |
+
),
|
155 |
+
"Content-Type": "application/json; charset=utf-8",
|
156 |
+
}
|
157 |
+
|
158 |
+
if self.access_token:
|
159 |
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
160 |
+
if access_token:
|
161 |
+
headers["Authorization"] = f"Bearer {access_token}"
|
162 |
+
if self.captcha_token:
|
163 |
+
headers["X-Captcha-Token"] = self.captcha_token
|
164 |
+
if self.device_id:
|
165 |
+
headers["X-Device-Id"] = self.device_id
|
166 |
+
return headers
|
167 |
+
|
168 |
+
async def _make_request(
|
169 |
+
self,
|
170 |
+
method: str,
|
171 |
+
url: str,
|
172 |
+
data: Optional[Dict[str, Any]] = None,
|
173 |
+
params: Optional[Dict[str, Any]] = None,
|
174 |
+
headers: Optional[Dict[str, str]] = None,
|
175 |
+
) -> Dict[str, Any]:
|
176 |
+
last_error = None
|
177 |
+
|
178 |
+
# url = "{proxy}{target}".format(proxy="https://pikpak.tjsky.top/", target=url)
|
179 |
+
# print(url)
|
180 |
+
|
181 |
+
for attempt in range(self.max_retries):
|
182 |
+
try:
|
183 |
+
response = await self._send_request(method, url, data, params, headers)
|
184 |
+
return await self._handle_response(response)
|
185 |
+
except PikpakRetryException as error:
|
186 |
+
logging.info(f"Retry attempt {attempt + 1}/{self.max_retries}")
|
187 |
+
last_error = error
|
188 |
+
except PikpakException:
|
189 |
+
raise
|
190 |
+
except httpx.HTTPError as error:
|
191 |
+
logging.error(
|
192 |
+
f"HTTP Error on attempt {attempt + 1}/{self.max_retries}: {str(error)}"
|
193 |
+
)
|
194 |
+
last_error = error
|
195 |
+
except Exception as error:
|
196 |
+
logging.error(
|
197 |
+
f"Unexpected error on attempt {attempt + 1}/{self.max_retries}: {str(error)}"
|
198 |
+
)
|
199 |
+
last_error = error
|
200 |
+
|
201 |
+
await asyncio.sleep(self.initial_backoff * (2**attempt))
|
202 |
+
|
203 |
+
# If we've exhausted all retries, raise an exception with the last error
|
204 |
+
raise PikpakException(f"Max retries reached. Last error: {str(last_error)}")
|
205 |
+
|
206 |
+
async def _send_request(self, method, url, data, params, headers):
|
207 |
+
req_headers = headers or self.get_headers()
|
208 |
+
return await self.httpx_client.request(
|
209 |
+
method,
|
210 |
+
url,
|
211 |
+
json=data,
|
212 |
+
params=params,
|
213 |
+
headers=req_headers,
|
214 |
+
)
|
215 |
+
|
216 |
+
async def _handle_response(self, response) -> Dict[str, Any]:
|
217 |
+
try:
|
218 |
+
json_data = response.json()
|
219 |
+
except ValueError:
|
220 |
+
if response.status_code == 200:
|
221 |
+
return {}
|
222 |
+
raise PikpakRetryException("Empty JSON data")
|
223 |
+
|
224 |
+
self.data_response = json_data
|
225 |
+
|
226 |
+
if not json_data:
|
227 |
+
if response.status_code == 200:
|
228 |
+
return {}
|
229 |
+
raise PikpakRetryException("Empty JSON data")
|
230 |
+
|
231 |
+
if "error" not in json_data:
|
232 |
+
return json_data
|
233 |
+
|
234 |
+
if "captcha_token" in json_data:
|
235 |
+
self.captcha_token = json_data["captcha_token"]
|
236 |
+
|
237 |
+
if json_data["error"] == "invalid_account_or_password":
|
238 |
+
raise PikpakException("Invalid username or password")
|
239 |
+
|
240 |
+
if json_data.get("error_code") == 16:
|
241 |
+
await self.refresh_access_token()
|
242 |
+
raise PikpakRetryException("Token refreshed, please retry")
|
243 |
+
|
244 |
+
raise PikpakException(json_data.get("error_description", "Unknown Error"))
|
245 |
+
|
246 |
+
async def _request_get(
|
247 |
+
self,
|
248 |
+
url: str,
|
249 |
+
params: dict = None,
|
250 |
+
):
|
251 |
+
return await self._make_request("get", url, params=params)
|
252 |
+
|
253 |
+
async def _request_post(
|
254 |
+
self,
|
255 |
+
url: str,
|
256 |
+
data: dict = None,
|
257 |
+
headers: dict = None,
|
258 |
+
):
|
259 |
+
return await self._make_request("post", url, data=data, headers=headers)
|
260 |
+
|
261 |
+
async def _request_patch(
|
262 |
+
self,
|
263 |
+
url: str,
|
264 |
+
data: dict = None,
|
265 |
+
):
|
266 |
+
return await self._make_request("patch", url, data=data)
|
267 |
+
|
268 |
+
async def _request_delete(
|
269 |
+
self,
|
270 |
+
url: str,
|
271 |
+
params: dict = None,
|
272 |
+
data: dict = None,
|
273 |
+
):
|
274 |
+
return await self._make_request("delete", url, params=params, data=data)
|
275 |
+
|
276 |
+
def decode_token(self):
|
277 |
+
"""Decodes the encoded token to update access and refresh tokens."""
|
278 |
+
try:
|
279 |
+
decoded_data = json.loads(b64decode(self.encoded_token).decode())
|
280 |
+
except (binascii.Error, json.JSONDecodeError):
|
281 |
+
raise PikpakException("Invalid encoded token")
|
282 |
+
if not decoded_data.get("access_token") or not decoded_data.get(
|
283 |
+
"refresh_token"
|
284 |
+
):
|
285 |
+
raise PikpakException("Invalid encoded token")
|
286 |
+
self.access_token = decoded_data.get("access_token")
|
287 |
+
self.refresh_token = decoded_data.get("refresh_token")
|
288 |
+
|
289 |
+
def encode_token(self):
|
290 |
+
"""Encodes the access and refresh tokens into a single string."""
|
291 |
+
token_data = {
|
292 |
+
"access_token": self.access_token,
|
293 |
+
"refresh_token": self.refresh_token,
|
294 |
+
}
|
295 |
+
self.encoded_token = b64encode(json.dumps(token_data).encode()).decode()
|
296 |
+
|
297 |
+
async def captcha_init(self, action: str, meta: dict = None) -> Dict[str, Any]:
|
298 |
+
url = f"https://{PikPakApi.PIKPAK_USER_HOST}/v1/shield/captcha/init"
|
299 |
+
if not meta:
|
300 |
+
t = f"{get_timestamp()}"
|
301 |
+
meta = {
|
302 |
+
"captcha_sign": captcha_sign(self.device_id, t),
|
303 |
+
"client_version": CLIENT_VERSION,
|
304 |
+
"package_name": PACKAG_ENAME,
|
305 |
+
"user_id": self.user_id,
|
306 |
+
"timestamp": t,
|
307 |
+
}
|
308 |
+
params = {
|
309 |
+
"client_id": CLIENT_ID,
|
310 |
+
"action": action,
|
311 |
+
"device_id": self.device_id,
|
312 |
+
"meta": meta,
|
313 |
+
}
|
314 |
+
return await self._request_post(url, data=params)
|
315 |
+
|
316 |
+
async def login(self) -> None:
|
317 |
+
"""
|
318 |
+
Login to PikPak
|
319 |
+
"""
|
320 |
+
login_url = f"https://{PikPakApi.PIKPAK_USER_HOST}/v1/auth/signin"
|
321 |
+
metas = {}
|
322 |
+
if not self.username or not self.password:
|
323 |
+
raise PikpakException("username and password are required")
|
324 |
+
if re.match(r"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", self.username):
|
325 |
+
metas["email"] = self.username
|
326 |
+
elif re.match(r"\d{11,18}", self.username):
|
327 |
+
metas["phone_number"] = self.username
|
328 |
+
else:
|
329 |
+
metas["username"] = self.username
|
330 |
+
result = await self.captcha_init(
|
331 |
+
action=f"POST:{login_url}",
|
332 |
+
meta=metas,
|
333 |
+
)
|
334 |
+
captcha_token = result.get("captcha_token", "")
|
335 |
+
if not captcha_token:
|
336 |
+
raise PikpakException("captcha_token get failed")
|
337 |
+
|
338 |
+
login_data = {
|
339 |
+
"client_id": CLIENT_ID,
|
340 |
+
"client_secret": CLIENT_SECRET,
|
341 |
+
"password": self.password,
|
342 |
+
"username": self.username,
|
343 |
+
"captcha_token": captcha_token,
|
344 |
+
}
|
345 |
+
user_info = await self._request_post(
|
346 |
+
login_url,
|
347 |
+
login_data,
|
348 |
+
{
|
349 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
350 |
+
},
|
351 |
+
)
|
352 |
+
self.access_token = user_info["access_token"]
|
353 |
+
self.refresh_token = user_info["refresh_token"]
|
354 |
+
self.user_id = user_info["sub"]
|
355 |
+
self.encode_token()
|
356 |
+
|
357 |
+
async def refresh_access_token(self) -> None:
|
358 |
+
"""
|
359 |
+
Refresh access token
|
360 |
+
"""
|
361 |
+
refresh_url = f"https://{self.PIKPAK_USER_HOST}/v1/auth/token"
|
362 |
+
refresh_data = {
|
363 |
+
"client_id": CLIENT_ID,
|
364 |
+
"refresh_token": self.refresh_token,
|
365 |
+
"grant_type": "refresh_token",
|
366 |
+
}
|
367 |
+
user_info = await self._request_post(refresh_url, refresh_data)
|
368 |
+
|
369 |
+
self.access_token = user_info["access_token"]
|
370 |
+
self.refresh_token = user_info["refresh_token"]
|
371 |
+
self.user_id = user_info["sub"]
|
372 |
+
self.encode_token()
|
373 |
+
if self.token_refresh_callback:
|
374 |
+
await self.token_refresh_callback(
|
375 |
+
self, **self.token_refresh_callback_kwargs
|
376 |
+
)
|
377 |
+
|
378 |
+
def get_user_info(self) -> Dict[str, Optional[str]]:
|
379 |
+
"""
|
380 |
+
Get user info
|
381 |
+
"""
|
382 |
+
return {
|
383 |
+
"username": self.username,
|
384 |
+
"user_id": self.user_id,
|
385 |
+
"access_token": self.access_token,
|
386 |
+
"refresh_token": self.refresh_token,
|
387 |
+
"encoded_token": self.encoded_token,
|
388 |
+
}
|
389 |
+
|
390 |
+
async def create_folder(
|
391 |
+
self, name: str = "新建文件夹", parent_id: Optional[str] = None
|
392 |
+
) -> Dict[str, Any]:
|
393 |
+
"""
|
394 |
+
name: str - 文件夹名称
|
395 |
+
parent_id: str - 父文件夹id, 默认创建到根目录
|
396 |
+
|
397 |
+
创建文件夹
|
398 |
+
"""
|
399 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files"
|
400 |
+
data = {
|
401 |
+
"kind": "drive#folder",
|
402 |
+
"name": name,
|
403 |
+
"parent_id": parent_id,
|
404 |
+
}
|
405 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files")
|
406 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
407 |
+
result = await self._request_post(url, data)
|
408 |
+
return result
|
409 |
+
|
410 |
+
async def delete_to_trash(self, ids: List[str]) -> Dict[str, Any]:
|
411 |
+
"""
|
412 |
+
ids: List[str] - 文件夹、文件id列表
|
413 |
+
|
414 |
+
将文件夹、文件移动到回收站
|
415 |
+
"""
|
416 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files:batchTrash"
|
417 |
+
data = {
|
418 |
+
"ids": ids,
|
419 |
+
}
|
420 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files:batchTrash")
|
421 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
422 |
+
result = await self._request_post(url, data)
|
423 |
+
return result
|
424 |
+
|
425 |
+
async def untrash(self, ids: List[str]) -> Dict[str, Any]:
|
426 |
+
"""
|
427 |
+
ids: List[str] - 文件夹、文件id列表
|
428 |
+
|
429 |
+
将文件夹、文件移出回收站
|
430 |
+
"""
|
431 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files:batchUntrash"
|
432 |
+
data = {
|
433 |
+
"ids": ids,
|
434 |
+
}
|
435 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files:batchUntrash")
|
436 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
437 |
+
result = await self._request_post(url, data)
|
438 |
+
return result
|
439 |
+
|
440 |
+
async def delete_forever(self, ids: List[str]) -> Dict[str, Any]:
|
441 |
+
"""
|
442 |
+
ids: List[str] - 文件夹、文件id列表
|
443 |
+
|
444 |
+
永远删除文件夹、文件, 慎用
|
445 |
+
"""
|
446 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files:batchDelete"
|
447 |
+
data = {
|
448 |
+
"ids": ids,
|
449 |
+
}
|
450 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files:batchDelete")
|
451 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
452 |
+
result = await self._request_post(url, data)
|
453 |
+
return result
|
454 |
+
|
455 |
+
async def offline_download(
|
456 |
+
self, file_url: str, parent_id: Optional[str] = None, name: Optional[str] = None
|
457 |
+
) -> Dict[str, Any]:
|
458 |
+
"""
|
459 |
+
file_url: str - 文件链接
|
460 |
+
parent_id: str - 父文件夹id, 不传默认存储到 My Pack
|
461 |
+
name: str - 文件名, 不传默认为文件链接的文件名
|
462 |
+
|
463 |
+
离线下载磁力链
|
464 |
+
"""
|
465 |
+
download_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files"
|
466 |
+
download_data = {
|
467 |
+
"kind": "drive#file",
|
468 |
+
"name": name,
|
469 |
+
"upload_type": "UPLOAD_TYPE_URL",
|
470 |
+
"url": {"url": file_url},
|
471 |
+
"folder_type": "DOWNLOAD" if not parent_id else "",
|
472 |
+
"parent_id": parent_id,
|
473 |
+
}
|
474 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files")
|
475 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
476 |
+
result = await self._request_post(download_url, download_data)
|
477 |
+
return result
|
478 |
+
|
479 |
+
async def offline_list(
|
480 |
+
self,
|
481 |
+
size: int = 10000,
|
482 |
+
next_page_token: Optional[str] = None,
|
483 |
+
phase: Optional[List[str]] = None,
|
484 |
+
) -> Dict[str, Any]:
|
485 |
+
"""
|
486 |
+
size: int - 每次请求的数量
|
487 |
+
next_page_token: str - 下一页的page token
|
488 |
+
phase: List[str] - Offline download task status, default is ["PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR"]
|
489 |
+
supported values: PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
|
490 |
+
|
491 |
+
获取离线下载列表
|
492 |
+
"""
|
493 |
+
if phase is None:
|
494 |
+
phase = ["PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR"]
|
495 |
+
list_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/tasks"
|
496 |
+
list_data = {
|
497 |
+
"type": "offline",
|
498 |
+
"thumbnail_size": "SIZE_SMALL",
|
499 |
+
"limit": size,
|
500 |
+
"page_token": next_page_token,
|
501 |
+
"filters": json.dumps({"phase": {"in": ",".join(phase)}}),
|
502 |
+
"with": "reference_resource",
|
503 |
+
}
|
504 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/tasks")
|
505 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
506 |
+
result = await self._request_get(list_url, list_data)
|
507 |
+
return result
|
508 |
+
|
509 |
+
async def offline_file_info(self, file_id: str) -> Dict[str, Any]:
|
510 |
+
"""
|
511 |
+
file_id: str - 离线下载文件id
|
512 |
+
|
513 |
+
离线下载文件信息
|
514 |
+
"""
|
515 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files/{file_id}")
|
516 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
517 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files/{file_id}"
|
518 |
+
result = await self._request_get(url, {"thumbnail_size": "SIZE_LARGE"})
|
519 |
+
return result
|
520 |
+
|
521 |
+
async def file_list(
|
522 |
+
self,
|
523 |
+
size: int = 100,
|
524 |
+
parent_id: Optional[str] = None,
|
525 |
+
next_page_token: Optional[str] = None,
|
526 |
+
additional_filters: Optional[Dict[str, Any]] = None,
|
527 |
+
) -> Dict[str, Any]:
|
528 |
+
"""
|
529 |
+
size: int - 每次请求的数量
|
530 |
+
parent_id: str - 父文件夹id, 默认列出根目录
|
531 |
+
next_page_token: str - 下一页的page token
|
532 |
+
additional_filters: Dict[str, Any] - 额外的过滤条件
|
533 |
+
|
534 |
+
获取文件列表,可以获得文件下载链接
|
535 |
+
"""
|
536 |
+
default_filters = {
|
537 |
+
"trashed": {"eq": False},
|
538 |
+
"phase": {"eq": "PHASE_TYPE_COMPLETE"},
|
539 |
+
}
|
540 |
+
if additional_filters:
|
541 |
+
default_filters.update(additional_filters)
|
542 |
+
list_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/files"
|
543 |
+
list_data = {
|
544 |
+
"parent_id": parent_id,
|
545 |
+
"thumbnail_size": "SIZE_MEDIUM",
|
546 |
+
"limit": size,
|
547 |
+
"with_audit": "true",
|
548 |
+
"page_token": next_page_token,
|
549 |
+
"filters": json.dumps(default_filters),
|
550 |
+
}
|
551 |
+
|
552 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/files")
|
553 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
554 |
+
result = await self._request_get(list_url, list_data)
|
555 |
+
return result
|
556 |
+
|
557 |
+
async def events(
|
558 |
+
self, size: int = 100, next_page_token: Optional[str] = None
|
559 |
+
) -> Dict[str, Any]:
|
560 |
+
"""
|
561 |
+
size: int - 每次请求的数量
|
562 |
+
next_page_token: str - 下一页的page token
|
563 |
+
|
564 |
+
获取最近添加事件列表
|
565 |
+
"""
|
566 |
+
list_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/events"
|
567 |
+
list_data = {
|
568 |
+
"thumbnail_size": "SIZE_MEDIUM",
|
569 |
+
"limit": size,
|
570 |
+
"next_page_token": next_page_token,
|
571 |
+
}
|
572 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/files")
|
573 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
574 |
+
result = await self._request_get(list_url, list_data)
|
575 |
+
return result
|
576 |
+
|
577 |
+
async def offline_task_retry(self, task_id: str) -> Dict[str, Any]:
|
578 |
+
"""
|
579 |
+
task_id: str - 离线下载任务id
|
580 |
+
|
581 |
+
重试离线下载任务
|
582 |
+
"""
|
583 |
+
list_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/task"
|
584 |
+
list_data = {
|
585 |
+
"type": "offline",
|
586 |
+
"create_type": "RETRY",
|
587 |
+
"id": task_id,
|
588 |
+
}
|
589 |
+
try:
|
590 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/task")
|
591 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
592 |
+
result = await self._request_post(list_url, list_data)
|
593 |
+
return result
|
594 |
+
except Exception as e:
|
595 |
+
raise PikpakException(f"重试离线下载任务失败: {task_id}. {e}")
|
596 |
+
|
597 |
+
async def delete_tasks(
|
598 |
+
self, task_ids: List[str], delete_files: bool = False
|
599 |
+
) -> None:
|
600 |
+
"""
|
601 |
+
delete tasks by task ids
|
602 |
+
task_ids: List[str] - task ids to delete
|
603 |
+
"""
|
604 |
+
delete_url = f"https://{self.PIKPAK_API_HOST}/drive/v1/tasks"
|
605 |
+
params = {
|
606 |
+
"task_ids": task_ids,
|
607 |
+
"delete_files": delete_files,
|
608 |
+
}
|
609 |
+
try:
|
610 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/tasks")
|
611 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
612 |
+
await self._request_delete(delete_url, params=params)
|
613 |
+
except Exception as e:
|
614 |
+
raise PikpakException(f"Failing to delete tasks: {task_ids}. {e}")
|
615 |
+
|
616 |
+
async def get_task_status(self, task_id: str, file_id: str) -> DownloadStatus:
|
617 |
+
"""
|
618 |
+
task_id: str - 离线下载任务id
|
619 |
+
file_id: str - 离线下载文件id
|
620 |
+
|
621 |
+
获取离线下载任务状态, 临时实现, 后期可能变更
|
622 |
+
"""
|
623 |
+
try:
|
624 |
+
infos = await self.offline_list()
|
625 |
+
if infos and infos.get("tasks", []):
|
626 |
+
for task in infos.get("tasks", []):
|
627 |
+
if task_id == task.get("id"):
|
628 |
+
return DownloadStatus.downloading
|
629 |
+
file_info = await self.offline_file_info(file_id=file_id)
|
630 |
+
if file_info:
|
631 |
+
return DownloadStatus.done
|
632 |
+
else:
|
633 |
+
return DownloadStatus.not_found
|
634 |
+
except PikpakException:
|
635 |
+
return DownloadStatus.error
|
636 |
+
|
637 |
+
async def path_to_id(self, path: str, create: bool = False) -> List[Dict[str, str]]:
|
638 |
+
"""
|
639 |
+
path: str - 路径
|
640 |
+
create: bool - 是否创建不存在的文件夹
|
641 |
+
|
642 |
+
将形如 /path/a/b 的路径转换为 文件夹的id
|
643 |
+
"""
|
644 |
+
if not path or len(path) <= 0:
|
645 |
+
return []
|
646 |
+
paths = path.split("/")
|
647 |
+
paths = [p.strip() for p in paths if len(p) > 0]
|
648 |
+
# 构造不同级别的path表达式,尝试找到距离目标最近的那一层
|
649 |
+
multi_level_paths = ["/" + "/".join(paths[: i + 1]) for i in range(len(paths))]
|
650 |
+
path_ids = [
|
651 |
+
self._path_id_cache[p]
|
652 |
+
for p in multi_level_paths
|
653 |
+
if p in self._path_id_cache
|
654 |
+
]
|
655 |
+
# 判断缓存命中情况
|
656 |
+
hit_cnt = len(path_ids)
|
657 |
+
if hit_cnt == len(paths):
|
658 |
+
return path_ids
|
659 |
+
elif hit_cnt == 0:
|
660 |
+
count = 0
|
661 |
+
parent_id = None
|
662 |
+
else:
|
663 |
+
count = hit_cnt
|
664 |
+
parent_id = path_ids[-1]["id"]
|
665 |
+
|
666 |
+
next_page_token = None
|
667 |
+
while count < len(paths):
|
668 |
+
data = await self.file_list(
|
669 |
+
parent_id=parent_id, next_page_token=next_page_token
|
670 |
+
)
|
671 |
+
record_of_target_path = None
|
672 |
+
for f in data.get("files", []):
|
673 |
+
current_path = "/" + "/".join(paths[:count] + [f.get("name")])
|
674 |
+
file_type = (
|
675 |
+
"folder" if f.get("kind", "").find("folder") != -1 else "file"
|
676 |
+
)
|
677 |
+
record = {
|
678 |
+
"id": f.get("id"),
|
679 |
+
"name": f.get("name"),
|
680 |
+
"file_type": file_type,
|
681 |
+
}
|
682 |
+
self._path_id_cache[current_path] = record
|
683 |
+
if f.get("name") == paths[count]:
|
684 |
+
record_of_target_path = record
|
685 |
+
# 不break: 剩下的文���也同样缓存起来
|
686 |
+
if record_of_target_path is not None:
|
687 |
+
path_ids.append(record_of_target_path)
|
688 |
+
count += 1
|
689 |
+
parent_id = record_of_target_path["id"]
|
690 |
+
elif data.get("next_page_token") and (
|
691 |
+
not next_page_token or next_page_token != data.get("next_page_token")
|
692 |
+
):
|
693 |
+
next_page_token = data.get("next_page_token")
|
694 |
+
elif create:
|
695 |
+
data = await self.create_folder(name=paths[count], parent_id=parent_id)
|
696 |
+
file_id = data.get("file").get("id")
|
697 |
+
record = {
|
698 |
+
"id": file_id,
|
699 |
+
"name": paths[count],
|
700 |
+
"file_type": "folder",
|
701 |
+
}
|
702 |
+
path_ids.append(record)
|
703 |
+
current_path = "/" + "/".join(paths[: count + 1])
|
704 |
+
self._path_id_cache[current_path] = record
|
705 |
+
count += 1
|
706 |
+
parent_id = file_id
|
707 |
+
else:
|
708 |
+
break
|
709 |
+
return path_ids
|
710 |
+
|
711 |
+
async def file_batch_move(
|
712 |
+
self,
|
713 |
+
ids: List[str],
|
714 |
+
to_parent_id: Optional[str] = None,
|
715 |
+
) -> Dict[str, Any]:
|
716 |
+
"""
|
717 |
+
ids: List[str] - 文件id列表
|
718 |
+
to_parent_id: str - 移动到的文件夹id, 默认为根目录
|
719 |
+
|
720 |
+
批量移动文件
|
721 |
+
"""
|
722 |
+
to = (
|
723 |
+
{
|
724 |
+
"parent_id": to_parent_id,
|
725 |
+
}
|
726 |
+
if to_parent_id
|
727 |
+
else {}
|
728 |
+
)
|
729 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/files:batchMove")
|
730 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
731 |
+
result = await self._request_post(
|
732 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files:batchMove",
|
733 |
+
data={
|
734 |
+
"ids": ids,
|
735 |
+
"to": to,
|
736 |
+
},
|
737 |
+
)
|
738 |
+
return result
|
739 |
+
|
740 |
+
async def file_batch_copy(
|
741 |
+
self,
|
742 |
+
ids: List[str],
|
743 |
+
to_parent_id: Optional[str] = None,
|
744 |
+
) -> Dict[str, Any]:
|
745 |
+
"""
|
746 |
+
ids: List[str] - 文件id列表
|
747 |
+
to_parent_id: str - 复制到的文件夹id, 默认为根目录
|
748 |
+
|
749 |
+
批量复制文件
|
750 |
+
"""
|
751 |
+
to = (
|
752 |
+
{
|
753 |
+
"parent_id": to_parent_id,
|
754 |
+
}
|
755 |
+
if to_parent_id
|
756 |
+
else {}
|
757 |
+
)
|
758 |
+
captcha_result = await self.captcha_init("GET:/drive/v1/files:batchCopy")
|
759 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
760 |
+
result = await self._request_post(
|
761 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files:batchCopy",
|
762 |
+
data={
|
763 |
+
"ids": ids,
|
764 |
+
"to": to,
|
765 |
+
},
|
766 |
+
)
|
767 |
+
return result
|
768 |
+
|
769 |
+
async def file_move_or_copy_by_path(
|
770 |
+
self,
|
771 |
+
from_path: List[str],
|
772 |
+
to_path: str,
|
773 |
+
move: bool = False,
|
774 |
+
create: bool = False,
|
775 |
+
) -> Dict[str, Any]:
|
776 |
+
"""
|
777 |
+
from_path: List[str] - 要移动或复制的文件路径列表
|
778 |
+
to_path: str - 移动或复制到的路径
|
779 |
+
is_move: bool - 是否移动, 默认为复制
|
780 |
+
create: bool - 是否创建不存在的文件夹
|
781 |
+
|
782 |
+
根据路径移动或复制文件
|
783 |
+
"""
|
784 |
+
from_ids: List[str] = []
|
785 |
+
for path in from_path:
|
786 |
+
if path_ids := await self.path_to_id(path):
|
787 |
+
if file_id := path_ids[-1].get("id"):
|
788 |
+
from_ids.append(file_id)
|
789 |
+
if not from_ids:
|
790 |
+
raise PikpakException("要移动的文件不存在")
|
791 |
+
to_path_ids = await self.path_to_id(to_path, create=create)
|
792 |
+
if to_path_ids:
|
793 |
+
to_parent_id = to_path_ids[-1].get("id")
|
794 |
+
else:
|
795 |
+
to_parent_id = None
|
796 |
+
if move:
|
797 |
+
result = await self.file_batch_move(ids=from_ids, to_parent_id=to_parent_id)
|
798 |
+
else:
|
799 |
+
result = await self.file_batch_copy(ids=from_ids, to_parent_id=to_parent_id)
|
800 |
+
return result
|
801 |
+
|
802 |
+
async def get_download_url(self, file_id: str) -> Dict[str, Any]:
|
803 |
+
"""
|
804 |
+
id: str - 文件id
|
805 |
+
|
806 |
+
Returns the file details data.
|
807 |
+
1. Use `medias[0][link][url]` for streaming with high speed in streaming services or tools.
|
808 |
+
2. Use `web_content_link` to download the file
|
809 |
+
"""
|
810 |
+
result = await self.captcha_init(
|
811 |
+
action=f"GET:/drive/v1/files/{file_id}",
|
812 |
+
)
|
813 |
+
self.captcha_token = result.get("captcha_token")
|
814 |
+
result = await self._request_get(
|
815 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files/{file_id}?",
|
816 |
+
)
|
817 |
+
self.captcha_token = None
|
818 |
+
return result
|
819 |
+
|
820 |
+
async def file_rename(self, id: str, new_file_name: str) -> Dict[str, Any]:
|
821 |
+
"""
|
822 |
+
id: str - 文件id
|
823 |
+
new_file_name: str - 新的文件名
|
824 |
+
|
825 |
+
重命名文件
|
826 |
+
返回文件的详细信息
|
827 |
+
"""
|
828 |
+
data = {
|
829 |
+
"name": new_file_name,
|
830 |
+
}
|
831 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files/{id}")
|
832 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
833 |
+
result = await self._request_patch(
|
834 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files/{id}",
|
835 |
+
data=data,
|
836 |
+
)
|
837 |
+
return result
|
838 |
+
|
839 |
+
async def file_batch_star(
|
840 |
+
self,
|
841 |
+
ids: List[str],
|
842 |
+
) -> Dict[str, Any]:
|
843 |
+
"""
|
844 |
+
ids: List[str] - 文件id列表
|
845 |
+
|
846 |
+
批量给文件加星标
|
847 |
+
"""
|
848 |
+
data = {
|
849 |
+
"ids": ids,
|
850 |
+
}
|
851 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files/star")
|
852 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
853 |
+
result = await self._request_post(
|
854 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files:star",
|
855 |
+
data=data,
|
856 |
+
)
|
857 |
+
return result
|
858 |
+
|
859 |
+
async def file_batch_unstar(
|
860 |
+
self,
|
861 |
+
ids: List[str],
|
862 |
+
) -> Dict[str, Any]:
|
863 |
+
"""
|
864 |
+
ids: List[str] - 文件id列表
|
865 |
+
|
866 |
+
批量给文件取消星标
|
867 |
+
"""
|
868 |
+
data = {
|
869 |
+
"ids": ids,
|
870 |
+
}
|
871 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/files/unstar")
|
872 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
873 |
+
result = await self._request_post(
|
874 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/files:unstar",
|
875 |
+
data=data,
|
876 |
+
)
|
877 |
+
return result
|
878 |
+
|
879 |
+
async def file_star_list(
|
880 |
+
self,
|
881 |
+
size: int = 100,
|
882 |
+
next_page_token: Optional[str] = None,
|
883 |
+
) -> Dict[str, Any]:
|
884 |
+
"""
|
885 |
+
size: int - 每次请求的数量
|
886 |
+
next_page_token: str - 下一页的page token
|
887 |
+
|
888 |
+
获取加星标的文件列表,可以获得文件下载链接
|
889 |
+
parent_id只可取默认值*,子目录列表通过获取星标目录以后自行使用file_list方法获取
|
890 |
+
"""
|
891 |
+
additional_filters = {"system_tag": {"in": "STAR"}}
|
892 |
+
result = await self.file_list(
|
893 |
+
size=size,
|
894 |
+
parent_id="*",
|
895 |
+
next_page_token=next_page_token,
|
896 |
+
additional_filters=additional_filters,
|
897 |
+
)
|
898 |
+
return result
|
899 |
+
|
900 |
+
async def file_batch_share(
|
901 |
+
self,
|
902 |
+
ids: List[str],
|
903 |
+
need_password: Optional[bool] = False,
|
904 |
+
expiration_days: Optional[int] = -1,
|
905 |
+
) -> Dict[str, Any]:
|
906 |
+
"""
|
907 |
+
ids: List[str] - 文件id列表
|
908 |
+
need_password: Optional[bool] - 是否需要分享密码
|
909 |
+
expiration_days: Optional[int] - 分享天数
|
910 |
+
|
911 |
+
批量分享文件,并生成分享链接
|
912 |
+
返回数据结构:
|
913 |
+
{
|
914 |
+
"share_id": "xxx", //分享ID
|
915 |
+
"share_url": "https://mypikpak.com/s/xxx", // 分享链接
|
916 |
+
"pass_code": "53fe", // 分享密码
|
917 |
+
"share_text": "https://mypikpak.com/s/xxx",
|
918 |
+
"share_list": []
|
919 |
+
}
|
920 |
+
"""
|
921 |
+
data = {
|
922 |
+
"file_ids": ids,
|
923 |
+
"share_to": "encryptedlink" if need_password else "publiclink",
|
924 |
+
"expiration_days": expiration_days,
|
925 |
+
"pass_code_option": "REQUIRED" if need_password else "NOT_REQUIRED",
|
926 |
+
}
|
927 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/share")
|
928 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
929 |
+
result = await self._request_post(
|
930 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/share",
|
931 |
+
data=data,
|
932 |
+
)
|
933 |
+
return result
|
934 |
+
|
935 |
+
async def get_quota_info(self) -> Dict[str, Any]:
|
936 |
+
"""
|
937 |
+
获取当前空间的quota信息
|
938 |
+
返回数据结构如下:
|
939 |
+
{
|
940 |
+
"kind": "drive#about",
|
941 |
+
"quota": {
|
942 |
+
"kind": "drive#quota",
|
943 |
+
"limit": "10995116277760", //空间总大小, 单位Byte
|
944 |
+
"usage": "5113157556024", // 已用空间大小,单位Byte
|
945 |
+
"usage_in_trash": "1281564700871", // 回收站占用大小,单位Byte
|
946 |
+
"play_times_limit": "-1",
|
947 |
+
"play_times_usage": "0"
|
948 |
+
},
|
949 |
+
"expires_at": "",
|
950 |
+
"quotas": {}
|
951 |
+
}
|
952 |
+
"""
|
953 |
+
# captcha_result = await self.captcha_init(f"GET:/drive/v1/about")
|
954 |
+
# self.captcha_token = captcha_result.get("captcha_token")
|
955 |
+
result = await self._request_get(
|
956 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/about",
|
957 |
+
)
|
958 |
+
return result
|
959 |
+
|
960 |
+
async def get_invite_code(self):
|
961 |
+
captcha_result = await self.captcha_init(f"GET:/vip/v1/activity/inviteCode")
|
962 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
963 |
+
result = await self._request_get(
|
964 |
+
url=f"https://{self.PIKPAK_API_HOST}/vip/v1/activity/inviteCode",
|
965 |
+
)
|
966 |
+
return result["code"]
|
967 |
+
|
968 |
+
async def vip_info(self):
|
969 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/privilege/vip")
|
970 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
971 |
+
result = await self._request_get(
|
972 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/privilege/vip",
|
973 |
+
)
|
974 |
+
return result
|
975 |
+
|
976 |
+
async def get_transfer_quota(self) -> Dict[str, Any]:
|
977 |
+
"""
|
978 |
+
Get transfer quota
|
979 |
+
"""
|
980 |
+
url = f"https://{self.PIKPAK_API_HOST}/vip/v1/quantity/list?type=transfer"
|
981 |
+
captcha_result = await self.captcha_init(
|
982 |
+
f"GET:/vip/v1/quantity/list?type=transfer"
|
983 |
+
)
|
984 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
985 |
+
result = await self._request_get(url)
|
986 |
+
return result
|
987 |
+
|
988 |
+
async def get_share_folder(
|
989 |
+
self, share_id: str, pass_code_token: str, parent_id: str = None
|
990 |
+
) -> Dict[str, Any]:
|
991 |
+
"""
|
992 |
+
获取分享链接下文件夹内容
|
993 |
+
|
994 |
+
Args:
|
995 |
+
share_id: str - 分享ID eg. /s/VO8BcRb-XXXXX 的 VO8BcRb-XXXXX
|
996 |
+
pass_code_token: str - 通过 get_share_info 获取到的 pass_code_token
|
997 |
+
parent_id: str - 父文件夹id, 默认列出根目录
|
998 |
+
"""
|
999 |
+
data = {
|
1000 |
+
"limit": "100",
|
1001 |
+
"thumbnail_size": "SIZE_LARGE",
|
1002 |
+
"order": "6",
|
1003 |
+
"share_id": share_id,
|
1004 |
+
"parent_id": parent_id,
|
1005 |
+
"pass_code_token": pass_code_token,
|
1006 |
+
}
|
1007 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/share/detail"
|
1008 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/share/detail")
|
1009 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
1010 |
+
return await self._request_get(url, params=data)
|
1011 |
+
|
1012 |
+
async def get_share_info(
|
1013 |
+
self, share_link: str, pass_code: str = None
|
1014 |
+
) -> ValueError | Dict[str, Any] | List[Dict[str | Any, str | Any]]:
|
1015 |
+
"""
|
1016 |
+
获取分享链接下内容
|
1017 |
+
|
1018 |
+
Args:
|
1019 |
+
share_link: str - 分享链接
|
1020 |
+
pass_code: str - 分享密码, 无密码则留空
|
1021 |
+
"""
|
1022 |
+
match = re.search(r"/s/([^/]+)(?:.*/([^/]+))?$", share_link)
|
1023 |
+
if match:
|
1024 |
+
share_id = match.group(1)
|
1025 |
+
parent_id = match.group(2) if match.group(2) else None
|
1026 |
+
else:
|
1027 |
+
return ValueError("Share Link Is Not Right")
|
1028 |
+
|
1029 |
+
data = {
|
1030 |
+
"limit": "100",
|
1031 |
+
"thumbnail_size": "SIZE_LARGE",
|
1032 |
+
"order": "3",
|
1033 |
+
"share_id": share_id,
|
1034 |
+
"parent_id": parent_id,
|
1035 |
+
"pass_code": pass_code,
|
1036 |
+
}
|
1037 |
+
url = f"https://{self.PIKPAK_API_HOST}/drive/v1/share"
|
1038 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/share")
|
1039 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
1040 |
+
return await self._request_get(url, params=data)
|
1041 |
+
|
1042 |
+
async def restore(
|
1043 |
+
self, share_id: str, pass_code_token: str, file_ids: List[str]
|
1044 |
+
) -> Dict[str, Any]:
|
1045 |
+
"""
|
1046 |
+
|
1047 |
+
Args:
|
1048 |
+
share_id: 分享链接eg. /s/VO8BcRb-XXXXX 的 VO8BcRb-XXXXX
|
1049 |
+
pass_code_token: get_share_info获取, 无密码则留空
|
1050 |
+
file_ids: 需要转存的文件/文件夹ID列表, get_share_info获取id值
|
1051 |
+
"""
|
1052 |
+
data = {
|
1053 |
+
"share_id": share_id,
|
1054 |
+
"pass_code_token": pass_code_token,
|
1055 |
+
"file_ids": file_ids,
|
1056 |
+
}
|
1057 |
+
captcha_result = await self.captcha_init(f"GET:/drive/v1/share/restore")
|
1058 |
+
self.captcha_token = captcha_result.get("captcha_token")
|
1059 |
+
result = await self._request_post(
|
1060 |
+
url=f"https://{self.PIKPAK_API_HOST}/drive/v1/share/restore", data=data
|
1061 |
+
)
|
1062 |
+
return result
|
pikpakapi/enums.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
|
3 |
+
|
4 |
+
class DownloadStatus(Enum):
|
5 |
+
not_downloading = "not_downloading"
|
6 |
+
downloading = "downloading"
|
7 |
+
done = "done"
|
8 |
+
error = "error"
|
9 |
+
not_found = "not_found"
|
pikpakapi/utils.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import hashlib
|
2 |
+
from uuid import uuid4
|
3 |
+
import time
|
4 |
+
|
5 |
+
CLIENT_ID = "ZQL_zwA4qhHcoe_2"
|
6 |
+
CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg"
|
7 |
+
CLIENT_VERSION = "1.06.0.2132"
|
8 |
+
PACKAG_ENAME = "com.thunder.downloader"
|
9 |
+
SDK_VERSION = "2.0.3.203100 "
|
10 |
+
APP_NAME = PACKAG_ENAME
|
11 |
+
|
12 |
+
|
13 |
+
def get_timestamp() -> int:
|
14 |
+
"""
|
15 |
+
Get current timestamp.
|
16 |
+
"""
|
17 |
+
return int(time.time() * 1000)
|
18 |
+
|
19 |
+
|
20 |
+
def device_id_generator() -> str:
|
21 |
+
"""
|
22 |
+
Generate a random device id.
|
23 |
+
"""
|
24 |
+
return str(uuid4()).replace("-", "")
|
25 |
+
|
26 |
+
|
27 |
+
SALTS = [
|
28 |
+
"kVy0WbPhiE4v6oxXZ88DvoA3Q",
|
29 |
+
"lON/AUoZKj8/nBtcE85mVbkOaVdVa",
|
30 |
+
"rLGffQrfBKH0BgwQ33yZofvO3Or",
|
31 |
+
"FO6HWqw",
|
32 |
+
"GbgvyA2",
|
33 |
+
"L1NU9QvIQIH7DTRt",
|
34 |
+
"y7llk4Y8WfYflt6",
|
35 |
+
"iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
|
36 |
+
"8C28RTXmVcco0",
|
37 |
+
"X5Xh",
|
38 |
+
"7xe25YUgfGgD0xW3ezFS",
|
39 |
+
"",
|
40 |
+
"CKCR",
|
41 |
+
"8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
|
42 |
+
"t2TeZBXKqbdP09Arh9C3",
|
43 |
+
]
|
44 |
+
|
45 |
+
|
46 |
+
def captcha_sign(device_id: str, timestamp: str) -> str:
|
47 |
+
"""
|
48 |
+
Generate a captcha sign.
|
49 |
+
|
50 |
+
在网页端的js中, 搜索 captcha_sign, 可以找到对应的js代码
|
51 |
+
|
52 |
+
"""
|
53 |
+
sign = CLIENT_ID + CLIENT_VERSION + PACKAG_ENAME + device_id + timestamp
|
54 |
+
for salt in SALTS:
|
55 |
+
sign = hashlib.md5((sign + salt).encode()).hexdigest()
|
56 |
+
return f"1.{sign}"
|
57 |
+
|
58 |
+
|
59 |
+
def generate_device_sign(device_id, package_name):
|
60 |
+
signature_base = f"{device_id}{package_name}1appkey"
|
61 |
+
|
62 |
+
# 计算 SHA-1 哈希
|
63 |
+
sha1_hash = hashlib.sha1()
|
64 |
+
sha1_hash.update(signature_base.encode("utf-8"))
|
65 |
+
sha1_result = sha1_hash.hexdigest()
|
66 |
+
|
67 |
+
# 计算 MD5 哈希
|
68 |
+
md5_hash = hashlib.md5()
|
69 |
+
md5_hash.update(sha1_result.encode("utf-8"))
|
70 |
+
md5_result = md5_hash.hexdigest()
|
71 |
+
|
72 |
+
device_sign = f"div101.{device_id}{md5_result}"
|
73 |
+
|
74 |
+
return device_sign
|
75 |
+
|
76 |
+
|
77 |
+
def build_custom_user_agent(device_id, user_id):
|
78 |
+
device_sign = generate_device_sign(device_id, PACKAG_ENAME)
|
79 |
+
|
80 |
+
user_agent_parts = [
|
81 |
+
f"ANDROID-{APP_NAME}/{CLIENT_VERSION}",
|
82 |
+
"protocolVersion/200",
|
83 |
+
"accesstype/",
|
84 |
+
f"clientid/{CLIENT_ID}",
|
85 |
+
f"clientversion/{CLIENT_VERSION}",
|
86 |
+
"action_type/",
|
87 |
+
"networktype/WIFI",
|
88 |
+
"sessionid/",
|
89 |
+
f"deviceid/{device_id}",
|
90 |
+
"providername/NONE",
|
91 |
+
f"devicesign/{device_sign}",
|
92 |
+
"refresh_token/",
|
93 |
+
f"sdkversion/{SDK_VERSION}",
|
94 |
+
f"datetime/{get_timestamp()}",
|
95 |
+
f"usrno/{user_id}",
|
96 |
+
f"appname/{APP_NAME}",
|
97 |
+
"session_origin/",
|
98 |
+
"grant_type/",
|
99 |
+
"appid/",
|
100 |
+
"clientip/",
|
101 |
+
"devicename/Xiaomi_M2004j7ac",
|
102 |
+
"osversion/13",
|
103 |
+
"platformversion/10",
|
104 |
+
"accessmode/",
|
105 |
+
"devicemodel/M2004J7AC",
|
106 |
+
]
|
107 |
+
|
108 |
+
return " ".join(user_agent_parts)
|
templates/index.html
ADDED
@@ -0,0 +1,1929 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
3 |
+
<html>
|
4 |
+
|
5 |
+
<head>
|
6 |
+
<meta charset="UTF-8" />
|
7 |
+
<title>Task</title>
|
8 |
+
<link rel="stylesheet" type="text/css" href="https://www.unpkg.com/[email protected]/dist/css/bootstrap.min.css" />
|
9 |
+
<link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet" />
|
10 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/fonts.min.css" rel="stylesheet" />
|
11 |
+
<link href="//unpkg.com/[email protected]/dist/css/layui.css" rel="stylesheet">
|
12 |
+
</head>
|
13 |
+
|
14 |
+
<body style="height:100%;">
|
15 |
+
<div id="root"></div>
|
16 |
+
<a id="back-to-top" href="#" class="btn btn-success btn-lg back-to-top" role="button"><i
|
17 |
+
class="mdi mdi-arrow-up"></i></a>
|
18 |
+
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
|
19 |
+
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
|
20 |
+
<script src="https://www.unpkg.com/[email protected]/dist/jquery.min.js"></script>
|
21 |
+
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
|
22 |
+
<script src="https://www.unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
23 |
+
<script src="https://unpkg.com/[email protected]/dist/react-bootstrap.min.js"></script>
|
24 |
+
<script src="https://unpkg.com/[email protected]/dist/redux.min.js"></script>
|
25 |
+
<script src="https://unpkg.com/[email protected]/umd/react-router-dom.min.js"></script>
|
26 |
+
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
|
27 |
+
<script src="https://unpkg.com/[email protected]/runtime.js"></script>
|
28 |
+
<script src="https://cdn.bootcdn.net/ajax/libs/babel-polyfill/7.12.1/polyfill.min.js"></script>
|
29 |
+
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
|
30 |
+
<script src="//unpkg.com/[email protected]/dist/layui.js"></script>
|
31 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ajv/8.17.1/ajv7.min.js"></script>
|
32 |
+
<script src="https://unpkg.com/@tanstack/[email protected]/build/umd/index.production.js"></script>
|
33 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mitt.umd.min.js"></script>
|
34 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script>
|
35 |
+
<script src="https://cdn.jsdelivr.net/npm/@fancyapps/[email protected]/dist/fancybox/fancybox.umd.js"></script>
|
36 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/[email protected]/dist/fancybox/fancybox.css" />
|
37 |
+
|
38 |
+
<style>
|
39 |
+
.bi {
|
40 |
+
display: inline-block;
|
41 |
+
width: 1rem;
|
42 |
+
height: 1rem;
|
43 |
+
}
|
44 |
+
|
45 |
+
/*
|
46 |
+
* Sidebar
|
47 |
+
*/
|
48 |
+
@media (min-width: 768px) {
|
49 |
+
.sidebar {
|
50 |
+
width: 100%;
|
51 |
+
}
|
52 |
+
|
53 |
+
.sidebar .offcanvas-lg {
|
54 |
+
position: -webkit-sticky;
|
55 |
+
position: sticky;
|
56 |
+
top: 48px;
|
57 |
+
}
|
58 |
+
|
59 |
+
.navbar-search {
|
60 |
+
display: block;
|
61 |
+
}
|
62 |
+
}
|
63 |
+
|
64 |
+
.sidebar .nav-link {
|
65 |
+
font-size: 0.875rem;
|
66 |
+
font-weight: 500;
|
67 |
+
}
|
68 |
+
|
69 |
+
.sidebar .nav-link.active {
|
70 |
+
color: #2470dc;
|
71 |
+
}
|
72 |
+
|
73 |
+
.sidebar-heading {
|
74 |
+
font-size: 0.75rem;
|
75 |
+
}
|
76 |
+
|
77 |
+
/*
|
78 |
+
* Navbar
|
79 |
+
*/
|
80 |
+
.navbar {
|
81 |
+
background-color: teal;
|
82 |
+
}
|
83 |
+
|
84 |
+
.navbar-brand {
|
85 |
+
padding-top: 0.75rem;
|
86 |
+
padding-bottom: 0.75rem;
|
87 |
+
/* background-color: rgba(0, 0, 0, .25);
|
88 |
+
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); */
|
89 |
+
}
|
90 |
+
|
91 |
+
.navbar .form-control {
|
92 |
+
padding: 0.75rem 1rem;
|
93 |
+
}
|
94 |
+
|
95 |
+
.bd-placeholder-img {
|
96 |
+
font-size: 1.125rem;
|
97 |
+
text-anchor: middle;
|
98 |
+
-webkit-user-select: none;
|
99 |
+
-moz-user-select: none;
|
100 |
+
user-select: none;
|
101 |
+
}
|
102 |
+
|
103 |
+
@media (min-width: 768px) {
|
104 |
+
.bd-placeholder-img-lg {
|
105 |
+
font-size: 3.5rem;
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
.b-example-divider {
|
110 |
+
width: 100%;
|
111 |
+
height: 3rem;
|
112 |
+
background-color: rgba(0, 0, 0, 0.1);
|
113 |
+
border: solid rgba(0, 0, 0, 0.15);
|
114 |
+
border-width: 1px 0;
|
115 |
+
box-shadow: inset 0 0.5em 1.5em rgba(0, 0, 0, 0.1),
|
116 |
+
inset 0 0.125em 0.5em rgba(0, 0, 0, 0.15);
|
117 |
+
}
|
118 |
+
|
119 |
+
.b-example-vr {
|
120 |
+
flex-shrink: 0;
|
121 |
+
width: 1.5rem;
|
122 |
+
height: 100vh;
|
123 |
+
}
|
124 |
+
|
125 |
+
.bi {
|
126 |
+
vertical-align: -0.125em;
|
127 |
+
fill: currentColor;
|
128 |
+
}
|
129 |
+
|
130 |
+
.nav-scroller {
|
131 |
+
position: relative;
|
132 |
+
z-index: 2;
|
133 |
+
height: 2.75rem;
|
134 |
+
overflow-y: hidden;
|
135 |
+
}
|
136 |
+
|
137 |
+
.nav-scroller .nav {
|
138 |
+
display: flex;
|
139 |
+
flex-wrap: nowrap;
|
140 |
+
padding-bottom: 1rem;
|
141 |
+
margin-top: -1px;
|
142 |
+
overflow-x: auto;
|
143 |
+
text-align: center;
|
144 |
+
white-space: nowrap;
|
145 |
+
-webkit-overflow-scrolling: touch;
|
146 |
+
}
|
147 |
+
|
148 |
+
.btn-bd-primary {
|
149 |
+
--bd-violet-bg: #712cf9;
|
150 |
+
--bd-violet-rgb: 112.520718, 44.062154, 249.437846;
|
151 |
+
|
152 |
+
--bs-btn-font-weight: 600;
|
153 |
+
--bs-btn-color: var(--bs-white);
|
154 |
+
--bs-btn-bg: var(--bd-violet-bg);
|
155 |
+
--bs-btn-border-color: var(--bd-violet-bg);
|
156 |
+
--bs-btn-hover-color: var(--bs-white);
|
157 |
+
--bs-btn-hover-bg: #6528e0;
|
158 |
+
--bs-btn-hover-border-color: #6528e0;
|
159 |
+
--bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
|
160 |
+
--bs-btn-active-color: var(--bs-btn-hover-color);
|
161 |
+
--bs-btn-active-bg: #5a23c8;
|
162 |
+
--bs-btn-active-border-color: #5a23c8;
|
163 |
+
}
|
164 |
+
|
165 |
+
.bd-mode-toggle {
|
166 |
+
z-index: 1500;
|
167 |
+
}
|
168 |
+
|
169 |
+
.bd-mode-toggle .dropdown-menu .active .bi {
|
170 |
+
display: block !important;
|
171 |
+
}
|
172 |
+
|
173 |
+
.back-to-top {
|
174 |
+
position: fixed;
|
175 |
+
bottom: 25px;
|
176 |
+
right: 25px;
|
177 |
+
display: none;
|
178 |
+
}
|
179 |
+
|
180 |
+
.leftsidebar {
|
181 |
+
height: 100%;
|
182 |
+
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
183 |
+
}
|
184 |
+
|
185 |
+
@media (min-width: 768px) {
|
186 |
+
.leftsidebar {
|
187 |
+
min-width: 15%;
|
188 |
+
}
|
189 |
+
}
|
190 |
+
|
191 |
+
@media (max-width: 768px) {
|
192 |
+
.leftsidebar {
|
193 |
+
max-width: 50%;
|
194 |
+
}
|
195 |
+
}
|
196 |
+
|
197 |
+
.bg-teal {
|
198 |
+
background-color: teal;
|
199 |
+
}
|
200 |
+
</style>
|
201 |
+
|
202 |
+
<script type="text/babel" data-presets="react" data-type="module">
|
203 |
+
window.layer = layui.layer;
|
204 |
+
//事件监听开始 通过修改localstorage实现跨页面事件监听
|
205 |
+
const emitter = mitt();
|
206 |
+
// 监听 localStorage 变化
|
207 |
+
window.addEventListener("storage", (event) => {
|
208 |
+
if (event.key === "event") {
|
209 |
+
const { type, data } = JSON.parse(event.newValue);
|
210 |
+
emitter.emit(type, data);
|
211 |
+
}
|
212 |
+
});
|
213 |
+
// 封装 emit 方法
|
214 |
+
const emitEvent = (type, data) => {
|
215 |
+
// 触发本地事件
|
216 |
+
emitter.emit(type, data);
|
217 |
+
const randomString = Math.random()
|
218 |
+
.toString(36)
|
219 |
+
.substring(2, 10); // 生成一个随机字符串确保event每次的值不一样,如果一样会不触发事件
|
220 |
+
const identity = `${Date.now()}-${randomString}`;
|
221 |
+
// 存储到 localStorage,以便其他页面能够接收到
|
222 |
+
localStorage.setItem(
|
223 |
+
"event",
|
224 |
+
JSON.stringify({ type, data, identity })
|
225 |
+
);
|
226 |
+
};
|
227 |
+
|
228 |
+
// 封装 on 方法
|
229 |
+
const onEvent = (type, callback) => {
|
230 |
+
emitter.on(type, callback);
|
231 |
+
};
|
232 |
+
|
233 |
+
// 封装 off 方法
|
234 |
+
const offEvent = (type, callback) => {
|
235 |
+
emitter.off(type, callback);
|
236 |
+
};
|
237 |
+
//事件监听结束
|
238 |
+
|
239 |
+
|
240 |
+
Fancybox.bind("[data-fancybox]", {
|
241 |
+
Toolbar: {
|
242 |
+
display: {
|
243 |
+
right: ["slideshow", "download", "thumbs", "close"],
|
244 |
+
},
|
245 |
+
},
|
246 |
+
Images: {
|
247 |
+
initialSize: "fit",
|
248 |
+
}
|
249 |
+
});
|
250 |
+
|
251 |
+
|
252 |
+
var settingStorage = localforage.createInstance({
|
253 |
+
name: "setting",
|
254 |
+
driver: localforage.LOCALSTORAGE
|
255 |
+
});
|
256 |
+
// settingStorage.setItem("category", { name: 'test', id: 1 });
|
257 |
+
// settingStorage.getItem('category').then(function (value) {
|
258 |
+
// console.log(value);
|
259 |
+
// }).catch(function (err) {
|
260 |
+
// console.log(err);
|
261 |
+
// });
|
262 |
+
// settingStorage.getItem('category', function (err, value) {
|
263 |
+
// console.log(value.name);
|
264 |
+
// });
|
265 |
+
|
266 |
+
|
267 |
+
const { createStore, combineReducers } = Redux;
|
268 |
+
// 从 localStorage 加载初始状态
|
269 |
+
const loadStateFromLocalStorage = () => {
|
270 |
+
try {
|
271 |
+
const serializedState = localStorage.getItem('settings');
|
272 |
+
if (serializedState === null) {
|
273 |
+
return {}; // 默认值
|
274 |
+
}
|
275 |
+
return JSON.parse(serializedState);
|
276 |
+
} catch (e) {
|
277 |
+
console.error("Could not load state from localStorage:", e);
|
278 |
+
return {}; // 默认值
|
279 |
+
}
|
280 |
+
};
|
281 |
+
// 保存状态到 localStorage
|
282 |
+
const saveStateToLocalStorage = (state) => {
|
283 |
+
try {
|
284 |
+
const serializedState = JSON.stringify(state);
|
285 |
+
localStorage.setItem('settings', serializedState);
|
286 |
+
} catch (e) {
|
287 |
+
console.error("Could not save state to localStorage:", e);
|
288 |
+
}
|
289 |
+
};
|
290 |
+
|
291 |
+
// 定义初始状态
|
292 |
+
const initialSettingsState = loadStateFromLocalStorage();
|
293 |
+
// 创建 settings Reducer
|
294 |
+
function settingsReducer(state = initialSettingsState, action) {
|
295 |
+
switch (action.type) {
|
296 |
+
case 'SAVE_SETTING':
|
297 |
+
return { ...state, ...action.payload };
|
298 |
+
default:
|
299 |
+
return state;
|
300 |
+
}
|
301 |
+
}
|
302 |
+
|
303 |
+
// 合并 Reducer(如果有多个)
|
304 |
+
const rootReducer = combineReducers({
|
305 |
+
settings: settingsReducer,
|
306 |
+
});
|
307 |
+
|
308 |
+
// 创建 Redux Store
|
309 |
+
const STORE = createStore(rootReducer);
|
310 |
+
|
311 |
+
// 订阅 Store 的变化,并将状态保存到 localStorage
|
312 |
+
STORE.subscribe(() => {
|
313 |
+
saveStateToLocalStorage(STORE.getState().settings);
|
314 |
+
});
|
315 |
+
|
316 |
+
|
317 |
+
//数据校验
|
318 |
+
// var ajv = new ajv7.default()
|
319 |
+
// const schema = {
|
320 |
+
// type: "object",
|
321 |
+
// properties: {
|
322 |
+
// foo: { type: "integer" },
|
323 |
+
// bar: { type: "string" }
|
324 |
+
// },
|
325 |
+
// required: ["foo"],
|
326 |
+
// additionalProperties: false
|
327 |
+
// }
|
328 |
+
|
329 |
+
// const validate = ajv.compile(schema)
|
330 |
+
|
331 |
+
// const data = {
|
332 |
+
// foo: 1,
|
333 |
+
// bar: "abc"
|
334 |
+
// }
|
335 |
+
// const valid = validate(data)
|
336 |
+
// if (!valid) console.log(validate.errors)
|
337 |
+
|
338 |
+
|
339 |
+
const bytesToSize = (bytes) => {
|
340 |
+
if (bytes === 0) return '0 B';
|
341 |
+
var k = 1024;
|
342 |
+
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
343 |
+
i = Math.floor(Math.log(bytes) / Math.log(k));
|
344 |
+
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
345 |
+
};
|
346 |
+
const formatDate = (date) => {
|
347 |
+
var d = new Date(date);
|
348 |
+
var year = d.getFullYear();
|
349 |
+
var month = d.getMonth() + 1;
|
350 |
+
var day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
|
351 |
+
var hour = d.getHours();
|
352 |
+
var minutes = d.getMinutes();
|
353 |
+
var seconds = d.getSeconds();
|
354 |
+
return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds;
|
355 |
+
};
|
356 |
+
|
357 |
+
let layerLoading = null;
|
358 |
+
|
359 |
+
const showLoading = () => {
|
360 |
+
const loadindex = layer.load(1);
|
361 |
+
layerLoading = loadindex;
|
362 |
+
}
|
363 |
+
|
364 |
+
const hideLoading = () => {
|
365 |
+
layer.close(layerLoading);
|
366 |
+
}
|
367 |
+
|
368 |
+
|
369 |
+
const { useState, useEffect, useRef } = React;
|
370 |
+
const { HashRouter, Route, Link, Switch, useLocation, useParams } = ReactRouterDOM;
|
371 |
+
const { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } = ReactQuery;
|
372 |
+
const queryClient = new QueryClient()
|
373 |
+
const {
|
374 |
+
Alert,
|
375 |
+
Badge,
|
376 |
+
Button,
|
377 |
+
ButtonGroup,
|
378 |
+
ButtonToolbar,
|
379 |
+
Card,
|
380 |
+
Collapse,
|
381 |
+
Col,
|
382 |
+
Container,
|
383 |
+
Dropdown,
|
384 |
+
Form,
|
385 |
+
Image,
|
386 |
+
InputGroup,
|
387 |
+
ListGroup,
|
388 |
+
Modal,
|
389 |
+
Nav,
|
390 |
+
Navbar,
|
391 |
+
NavDropdown,
|
392 |
+
Offcanvas,
|
393 |
+
Pagination,
|
394 |
+
Row,
|
395 |
+
Table,
|
396 |
+
} = ReactBootstrap;
|
397 |
+
//注意修改js文件后需要直接访问js以更新浏览器缓存
|
398 |
+
|
399 |
+
// 表格组件
|
400 |
+
const DataTable = ({ data, columns }) => {
|
401 |
+
return (
|
402 |
+
<Table responsive bordered>
|
403 |
+
<thead>
|
404 |
+
<tr className="text-center">
|
405 |
+
{columns.map((column, index) => (
|
406 |
+
<th key={index}>{column.title}</th>
|
407 |
+
))}
|
408 |
+
</tr>
|
409 |
+
</thead>
|
410 |
+
<tbody>
|
411 |
+
{data.map((row, rowIndex) => (
|
412 |
+
<tr key={rowIndex} className="text-center">
|
413 |
+
{columns.map((column, colIndex) => (
|
414 |
+
<td key={colIndex}>
|
415 |
+
{/* 调用渲染方法,如果没有定义,则直接显示数据 */}
|
416 |
+
{column.render
|
417 |
+
? column.render(row)
|
418 |
+
: row[column.dataIndex]}
|
419 |
+
</td>
|
420 |
+
))}
|
421 |
+
</tr>
|
422 |
+
))}
|
423 |
+
</tbody>
|
424 |
+
</Table>
|
425 |
+
);
|
426 |
+
};
|
427 |
+
//分页组件
|
428 |
+
const Paginate = (props) => {
|
429 |
+
const page = props.page;
|
430 |
+
const pageCount = Math.ceil(
|
431 |
+
props.totalCount / props.itemsPerPage
|
432 |
+
);
|
433 |
+
|
434 |
+
const SelectItems = () => {
|
435 |
+
const pageNumbers = Array.from(
|
436 |
+
{ length: pageCount },
|
437 |
+
(_, i) => i + 1
|
438 |
+
);
|
439 |
+
return (
|
440 |
+
<select
|
441 |
+
className="page-link border-0 h-100 py-0"
|
442 |
+
style={{ width: "auto" }}
|
443 |
+
onChange={(e) => {
|
444 |
+
props.onClick(parseInt(e.target.value));
|
445 |
+
}}
|
446 |
+
>
|
447 |
+
{pageNumbers.map((number) => {
|
448 |
+
const selected = number === page ? true : false;
|
449 |
+
return (
|
450 |
+
<option
|
451 |
+
key={number}
|
452 |
+
value={number}
|
453 |
+
selected={selected}
|
454 |
+
>
|
455 |
+
{number}
|
456 |
+
</option>
|
457 |
+
);
|
458 |
+
})}
|
459 |
+
</select>
|
460 |
+
);
|
461 |
+
};
|
462 |
+
return (
|
463 |
+
<div className="d-flex justify-content-center align-items-baseline">
|
464 |
+
|
465 |
+
<Pagination>
|
466 |
+
{pageCount > 1 && page > 1 && (
|
467 |
+
<Pagination.First
|
468 |
+
onClick={() => {
|
469 |
+
props.onClick(1);
|
470 |
+
}}
|
471 |
+
/>
|
472 |
+
)}
|
473 |
+
{pageCount > 1 && page > 1 && (
|
474 |
+
<Pagination.Prev
|
475 |
+
onClick={() => {
|
476 |
+
props.onClick(page - 1);
|
477 |
+
}}
|
478 |
+
/>
|
479 |
+
)}
|
480 |
+
<Pagination.Item linkClassName="p-0 h-100 d-inline-block">
|
481 |
+
<SelectItems />
|
482 |
+
</Pagination.Item>
|
483 |
+
<Pagination.Item>
|
484 |
+
<span className="text-info">
|
485 |
+
{page}/{pageCount}
|
486 |
+
</span>
|
487 |
+
</Pagination.Item>
|
488 |
+
{pageCount > 1 && page < pageCount && (
|
489 |
+
<Pagination.Next
|
490 |
+
onClick={() => {
|
491 |
+
props.onClick(page + 1);
|
492 |
+
}}
|
493 |
+
/>
|
494 |
+
)}
|
495 |
+
{pageCount > 1 && page < pageCount && (
|
496 |
+
<Pagination.Last
|
497 |
+
onClick={() => {
|
498 |
+
props.onClick(pageCount);
|
499 |
+
}}
|
500 |
+
/>
|
501 |
+
)}
|
502 |
+
</Pagination>
|
503 |
+
</div>
|
504 |
+
);
|
505 |
+
};
|
506 |
+
//图标组件
|
507 |
+
const Icon = (props) => {
|
508 |
+
return (
|
509 |
+
<span
|
510 |
+
onClick={props.onClick}
|
511 |
+
className={`mdi mdi-${props.icon} fs-${props.size} ${props.className}`}
|
512 |
+
></span>
|
513 |
+
);
|
514 |
+
};
|
515 |
+
//按钮图标组件
|
516 |
+
const IconButton = (props) => {
|
517 |
+
return (
|
518 |
+
<Button
|
519 |
+
variant="success"
|
520 |
+
onClick={props.onClick}
|
521 |
+
className={props.className}
|
522 |
+
>
|
523 |
+
<span
|
524 |
+
className={`mdi mdi-${props.icon} fs-${props.iconSize} ${props.iconClassName}`}
|
525 |
+
></span>
|
526 |
+
{props.text}
|
527 |
+
</Button>
|
528 |
+
);
|
529 |
+
};
|
530 |
+
//video组件
|
531 |
+
const createCaption = (video) => {
|
532 |
+
var html = video.code + ' ' + video.title;
|
533 |
+
//html+="<a href='/tags'>test</a>";
|
534 |
+
video.tags.map(tag => {
|
535 |
+
html += tag;
|
536 |
+
})
|
537 |
+
return html;
|
538 |
+
};
|
539 |
+
|
540 |
+
|
541 |
+
const AsyncImage = (props) => {
|
542 |
+
const [loadedSrc, setLoadedSrc] = React.useState(null);
|
543 |
+
React.useEffect(() => {
|
544 |
+
setLoadedSrc(null);
|
545 |
+
if (props.src) {
|
546 |
+
const handleLoad = () => {
|
547 |
+
setLoadedSrc(props.src);
|
548 |
+
};
|
549 |
+
const image = document.createElement("img");
|
550 |
+
image.addEventListener('load', handleLoad);
|
551 |
+
image.src = props.src;
|
552 |
+
return () => {
|
553 |
+
image.removeEventListener('load', handleLoad);
|
554 |
+
};
|
555 |
+
}
|
556 |
+
}, [props.src]);
|
557 |
+
if (loadedSrc === props.src) {
|
558 |
+
return (
|
559 |
+
<img {...props} />
|
560 |
+
);
|
561 |
+
}
|
562 |
+
return <img {...props} src="https://placehold.co/600x400?text=Loading" />;
|
563 |
+
};
|
564 |
+
|
565 |
+
const Video = ({ video }) => {
|
566 |
+
|
567 |
+
const [showPreview, setShowPreview] = useState(null);
|
568 |
+
const [holdPreviews, setHoldPreviews] = useState([]);
|
569 |
+
const videoRef = useRef(null);
|
570 |
+
|
571 |
+
|
572 |
+
const clickPreview = (id) => {
|
573 |
+
if (!holdPreviews.includes(id)) {
|
574 |
+
setHoldPreviews((prev) => [...prev, id]);
|
575 |
+
playPreview(id);
|
576 |
+
}
|
577 |
+
};
|
578 |
+
|
579 |
+
const playPreview = (id) => {
|
580 |
+
const preview = document.getElementById(`preview-${id}`);
|
581 |
+
|
582 |
+
if (!preview.getAttribute("src")) {
|
583 |
+
preview.addEventListener("loadedmetadata", (event) => {
|
584 |
+
event.target.play();
|
585 |
+
setShowPreview(id);
|
586 |
+
});
|
587 |
+
|
588 |
+
preview.setAttribute("src", preview.getAttribute("data-src"));
|
589 |
+
} else {
|
590 |
+
preview.play();
|
591 |
+
setShowPreview(id);
|
592 |
+
}
|
593 |
+
};
|
594 |
+
|
595 |
+
|
596 |
+
const { mutateAsync: favoriteMutation } = useMutation({
|
597 |
+
mutationKey: ["switch-favorite"],
|
598 |
+
mutationFn: async (favorite) => {
|
599 |
+
showLoading();
|
600 |
+
return await axios.post("/favorite", favorite)
|
601 |
+
},
|
602 |
+
onSuccess: async (data, variables, context) => {
|
603 |
+
hideLoading();
|
604 |
+
layer.msg('操作成功', { time: 2000, icon: 6 });
|
605 |
+
},
|
606 |
+
onError: () => {
|
607 |
+
hideLoading();
|
608 |
+
layer.msg('操作失败', { time: 2000, icon: 5 });
|
609 |
+
}
|
610 |
+
})
|
611 |
+
|
612 |
+
return (
|
613 |
+
<Card>
|
614 |
+
<a href={video.poster} target="_blank" data-fancybox="gallery" className={holdPreviews.includes(video.id) ? 'd-none' : 'd-block'}
|
615 |
+
data-download-src={video.src} data-caption={createCaption(video)}>
|
616 |
+
<AsyncImage className="card-img-top" src={video.poster} />
|
617 |
+
</a>
|
618 |
+
<video
|
619 |
+
className={holdPreviews.includes(video.id) ? 'd-block' : 'd-none'}
|
620 |
+
id={`preview-${video.id}`}
|
621 |
+
ref={videoRef}
|
622 |
+
data-src={`https://fourhoi.com/${video.code.toLowerCase()}/preview.mp4`}
|
623 |
+
controls
|
624 |
+
autoPlay
|
625 |
+
webkit-playsinline="true"
|
626 |
+
playsinline="true"
|
627 |
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
628 |
+
/>
|
629 |
+
|
630 |
+
<Card.Body>
|
631 |
+
<Card.Title>{`${video.code} ${video.title}`}</Card.Title>
|
632 |
+
<Card.Text>
|
633 |
+
{video.tags.map((tag, index) => (
|
634 |
+
<a target="_blank" href={`#/tags/${encodeURIComponent(tag)}`} className="badge bg-secondary text-wrap m-1">{tag}</a>
|
635 |
+
))}
|
636 |
+
</Card.Text>
|
637 |
+
|
638 |
+
<Dropdown className="float-start">
|
639 |
+
<Dropdown.Toggle variant="primary" id="dropdown-basic">
|
640 |
+
播放
|
641 |
+
</Dropdown.Toggle>
|
642 |
+
<Dropdown.Menu>
|
643 |
+
<Dropdown.Item href={video.src} target="_blank">网页</Dropdown.Item>
|
644 |
+
<Dropdown.Item href={`vlc://${video.src}`} target="_blank">VLC</Dropdown.Item>
|
645 |
+
<Dropdown.Item href={`videoplayerapp://open?url=${encodeURIComponent(video.src)}`} target="_blank">Video Player</Dropdown.Item>
|
646 |
+
</Dropdown.Menu>
|
647 |
+
</Dropdown>
|
648 |
+
|
649 |
+
|
650 |
+
<Button variant="primary" onClick={() => clickPreview(video.id)}><Icon
|
651 |
+
icon="video"
|
652 |
+
size="8"
|
653 |
+
className="text-text-primary"
|
654 |
+
/></Button>
|
655 |
+
|
656 |
+
|
657 |
+
<Button className={`btn float-end ${video.favorite ? 'btn-danger' : 'btn-secondary'}`} onClick={async () => {
|
658 |
+
layer.confirm('确定要进行操作?不可撤销!', {
|
659 |
+
btn: ['确定', '取消'] //按钮
|
660 |
+
}, async function () {
|
661 |
+
await favoriteMutation({ id: video.id, favorite: !video.favorite })
|
662 |
+
}, async function () {
|
663 |
+
|
664 |
+
});
|
665 |
+
|
666 |
+
}}><Icon
|
667 |
+
icon="star"
|
668 |
+
size="8"
|
669 |
+
className="text-text-primary"
|
670 |
+
/></Button>
|
671 |
+
|
672 |
+
|
673 |
+
|
674 |
+
</Card.Body>
|
675 |
+
</Card>
|
676 |
+
)
|
677 |
+
}
|
678 |
+
|
679 |
+
//设置框
|
680 |
+
const SettingModal = (props) => {
|
681 |
+
const settings = [
|
682 |
+
{ "alist": [{ "label": "Alist地址", "key": "alist_host", "show": true }, { "label": "Alist令牌", "key": "alist_token", "show": false }] },
|
683 |
+
{ "github": [{ "label": "Actions地址", "key": "github_host", "show": true }, { "label": "Github令牌", "key": "github_token", "show": false }] },
|
684 |
+
{ "directus": [{ "label": "Directus地址", "key": "directus_host", "show": true }, { "label": "Directus令牌", "key": "directus_token", "show": false }] }
|
685 |
+
]
|
686 |
+
const [setting, setSetting] = useState({});
|
687 |
+
// useEffect(() => {
|
688 |
+
// localStorage.setItem('settings', JSON.stringify(setting));
|
689 |
+
// }, [setting]);
|
690 |
+
|
691 |
+
const loadSetting = () => {
|
692 |
+
const storedSettings = STORE.getState().settings;
|
693 |
+
if (storedSettings) {
|
694 |
+
setSetting(storedSettings);
|
695 |
+
}
|
696 |
+
}
|
697 |
+
const saveSetting = () => {
|
698 |
+
STORE.dispatch({ type: 'SAVE_SETTING', payload: setting })
|
699 |
+
//localStorage.setItem('settings', JSON.stringify(setting));
|
700 |
+
}
|
701 |
+
return (
|
702 |
+
<Modal show={props.show} onHide={props.onHide} onShow={loadSetting}>
|
703 |
+
<Modal.Header closeButton onHide={props.onHide}>
|
704 |
+
<Modal.Title>设置</Modal.Title>
|
705 |
+
</Modal.Header>
|
706 |
+
<Modal.Body>
|
707 |
+
<Form>
|
708 |
+
<ListGroup>
|
709 |
+
{settings.map((value, index) => {
|
710 |
+
const key = Object.keys(value)[0];
|
711 |
+
const items = value[key];
|
712 |
+
return (<ListGroup.Item>
|
713 |
+
{items.map((setting_item) => {
|
714 |
+
return (
|
715 |
+
<Form.Group as={Row} className="mb-3">
|
716 |
+
<Form.Label column sm="3">
|
717 |
+
{setting_item.label}
|
718 |
+
</Form.Label>
|
719 |
+
<Col sm="9">
|
720 |
+
<Form.Control type={setting_item.show ? "input" : "password"} value={setting[setting_item.key]} name={setting_item.key} placeholder={setting_item.label} onChange={(e) => { setSetting({ ...setting, [setting_item.key]: e.target.value }) }} />
|
721 |
+
</Col>
|
722 |
+
</Form.Group>
|
723 |
+
)
|
724 |
+
})}
|
725 |
+
</ListGroup.Item>)
|
726 |
+
})}
|
727 |
+
</ListGroup>
|
728 |
+
</Form>
|
729 |
+
</Modal.Body>
|
730 |
+
<Modal.Footer className="justify-content-between">
|
731 |
+
<Button
|
732 |
+
variant="secondary"
|
733 |
+
onClick={() => {
|
734 |
+
props.onHide();
|
735 |
+
}}
|
736 |
+
>
|
737 |
+
关闭
|
738 |
+
</Button>
|
739 |
+
<Button
|
740 |
+
variant="primary"
|
741 |
+
onClick={() => {
|
742 |
+
saveSetting();
|
743 |
+
props.onHide();
|
744 |
+
//props.onSave();
|
745 |
+
}}
|
746 |
+
>
|
747 |
+
保存
|
748 |
+
</Button>
|
749 |
+
</Modal.Footer>
|
750 |
+
</Modal>
|
751 |
+
);
|
752 |
+
};
|
753 |
+
|
754 |
+
//axios封装开始
|
755 |
+
const useAxios = () => {
|
756 |
+
const [response, setResponse] = useState(null);
|
757 |
+
const [error, setError] = useState("");
|
758 |
+
const [loading, setLoading] = useState(false);
|
759 |
+
|
760 |
+
// Create an Axios instance
|
761 |
+
const axiosInstance = axios.create({});
|
762 |
+
|
763 |
+
// Set up request and response interceptors
|
764 |
+
axiosInstance.interceptors.request.use(
|
765 |
+
(config) => {
|
766 |
+
// Log or modify request here
|
767 |
+
//console.log("Sending request to:", config.url);
|
768 |
+
return config;
|
769 |
+
},
|
770 |
+
(error) => {
|
771 |
+
// Handle request error here
|
772 |
+
return Promise.reject(error);
|
773 |
+
}
|
774 |
+
);
|
775 |
+
|
776 |
+
axiosInstance.interceptors.response.use(
|
777 |
+
(response) => {
|
778 |
+
// Log or modify response here
|
779 |
+
//console.log("Received response from:", response.config.url);
|
780 |
+
return response;
|
781 |
+
},
|
782 |
+
(error) => {
|
783 |
+
// Handle response error here
|
784 |
+
return Promise.reject(error);
|
785 |
+
}
|
786 |
+
);
|
787 |
+
|
788 |
+
useEffect(() => {
|
789 |
+
const source = axios.CancelToken.source();
|
790 |
+
return () => {
|
791 |
+
// Cancel the request when the component unmounts
|
792 |
+
source.cancel(
|
793 |
+
"组件被卸载: 请求取消."
|
794 |
+
);
|
795 |
+
};
|
796 |
+
}, []);
|
797 |
+
|
798 |
+
// Making the API call with cancellation support
|
799 |
+
const fetchData = async ({ url, method, data, headers }) => {
|
800 |
+
setLoading(true);
|
801 |
+
try {
|
802 |
+
const result = await axiosInstance({
|
803 |
+
url,
|
804 |
+
method,
|
805 |
+
headers: headers ? headers : {},
|
806 |
+
data:
|
807 |
+
method.toLowerCase() === "get"
|
808 |
+
? undefined
|
809 |
+
: data,
|
810 |
+
params:
|
811 |
+
method.toLowerCase() === "get"
|
812 |
+
? data
|
813 |
+
: undefined,
|
814 |
+
cancelToken: axios.CancelToken.source().token,
|
815 |
+
});
|
816 |
+
setResponse(result.data);
|
817 |
+
} catch (error) {
|
818 |
+
if (axios.isCancel(error)) {
|
819 |
+
console.log("Request cancelled", error.message);
|
820 |
+
} else {
|
821 |
+
setError(
|
822 |
+
error.response
|
823 |
+
? error.response.data
|
824 |
+
: error.message
|
825 |
+
);
|
826 |
+
}
|
827 |
+
} finally {
|
828 |
+
setLoading(false);
|
829 |
+
}
|
830 |
+
};
|
831 |
+
return [response, error, loading, fetchData];
|
832 |
+
};
|
833 |
+
//axios封闭结束
|
834 |
+
|
835 |
+
//分页hooks
|
836 |
+
const usePagination = () => {
|
837 |
+
const [pagination, setPagination] = useState({
|
838 |
+
pageSize: 36,
|
839 |
+
pageIndex: 1,
|
840 |
+
});
|
841 |
+
const { pageSize, pageIndex } = pagination;
|
842 |
+
|
843 |
+
|
844 |
+
return {
|
845 |
+
limit: pageSize,
|
846 |
+
onPaginationChange: setPagination,
|
847 |
+
pagination,
|
848 |
+
skip: pageSize * (pageIndex - 1),
|
849 |
+
};
|
850 |
+
}
|
851 |
+
//分页结束
|
852 |
+
|
853 |
+
|
854 |
+
//API定义开始
|
855 |
+
const getFiles = () => {
|
856 |
+
const [response, error, loading, fetchData] = useAxios();
|
857 |
+
|
858 |
+
const fetchDataByPage = async (setting, query) => {
|
859 |
+
var host = setting.alist_host;
|
860 |
+
if (!host.endsWith("/")) {
|
861 |
+
host = host + '/'
|
862 |
+
}
|
863 |
+
fetchData({
|
864 |
+
url: host + 'api/fs/list',
|
865 |
+
method: "POST",
|
866 |
+
data: query,
|
867 |
+
headers: {
|
868 |
+
'Authorization': setting.alist_token,
|
869 |
+
'Content-Type': 'application/json'
|
870 |
+
},
|
871 |
+
});
|
872 |
+
};
|
873 |
+
return [response, error, loading, fetchDataByPage];
|
874 |
+
};
|
875 |
+
|
876 |
+
const paginateLinksGet = async (limit, page, keyword) => {
|
877 |
+
const url = `/video?size=${limit}&page=${page}&kw=${keyword}`;
|
878 |
+
const { data } = await axios.get(url)
|
879 |
+
return data
|
880 |
+
}
|
881 |
+
|
882 |
+
const paginateFavoritesGet = async (limit, page, keyword) => {
|
883 |
+
const url = `/favorites?size=${limit}&page=${page}&kw=${keyword}`;
|
884 |
+
const { data } = await axios.get(url)
|
885 |
+
return data
|
886 |
+
}
|
887 |
+
|
888 |
+
|
889 |
+
const paginateTagLinksGet = async (limit, page, tag) => {
|
890 |
+
const url = `/tags?size=${limit}&page=${page}&tag=${tag}`;
|
891 |
+
const { data } = await axios.get(url)
|
892 |
+
return data
|
893 |
+
}
|
894 |
+
|
895 |
+
const paginateTasksGet = async (limit, skip) => {
|
896 |
+
const setting = STORE.getState().settings;
|
897 |
+
const url = setting.directus_host + `items/task?limit=${limit}&offset=${skip}&meta[]=filter_count&sort[]=-id`;
|
898 |
+
const { data } = await axios.get(url, { headers: { Authorization: "Bearer " + setting.directus_token } })
|
899 |
+
return data
|
900 |
+
}
|
901 |
+
|
902 |
+
|
903 |
+
//API定义结束
|
904 |
+
|
905 |
+
const Layout = ({ children }) => {
|
906 |
+
useEffect(() => {
|
907 |
+
// 组件挂载时执行的代码(相当于 componentDidMount)
|
908 |
+
}, []); // 空数组表示只在挂载和卸载时执行
|
909 |
+
|
910 |
+
const [showSideBar, setShowSideBar] = useState(false);
|
911 |
+
const handleSidebarClose = () => setShowSideBar(false);
|
912 |
+
const handleSidebarShow = () => setShowSideBar(true);
|
913 |
+
const toggleSidebarShow = () => {
|
914 |
+
setShowSideBar(!showSideBar);
|
915 |
+
};
|
916 |
+
|
917 |
+
const [setting, setSetting] = useState(false);
|
918 |
+
|
919 |
+
return (
|
920 |
+
<div>
|
921 |
+
<header className="sticky-top">
|
922 |
+
<Navbar expand="md">
|
923 |
+
<Container fluid>
|
924 |
+
<div>
|
925 |
+
<Navbar.Toggle
|
926 |
+
className="shadow-none border-0"
|
927 |
+
onClick={handleSidebarShow}
|
928 |
+
children={
|
929 |
+
<Icon
|
930 |
+
icon="menu"
|
931 |
+
size="3"
|
932 |
+
className="text-white"
|
933 |
+
/>
|
934 |
+
}
|
935 |
+
/>
|
936 |
+
<Navbar.Brand
|
937 |
+
as={Link}
|
938 |
+
to="/"
|
939 |
+
className="text-white"
|
940 |
+
>
|
941 |
+
收藏
|
942 |
+
</Navbar.Brand>
|
943 |
+
</div>
|
944 |
+
<div className="d-flex">
|
945 |
+
<Tasks />
|
946 |
+
<LocalTasks />
|
947 |
+
<Button
|
948 |
+
style={{
|
949 |
+
backgroundColor: "transparent",
|
950 |
+
}}
|
951 |
+
className="nav-link btn"
|
952 |
+
onClick={() => {
|
953 |
+
setSetting(true)
|
954 |
+
}}
|
955 |
+
children={
|
956 |
+
<Icon
|
957 |
+
icon="dots-vertical"
|
958 |
+
size="3"
|
959 |
+
className="text-white"
|
960 |
+
/>
|
961 |
+
}
|
962 |
+
></Button>
|
963 |
+
<SettingModal
|
964 |
+
show={setting}
|
965 |
+
onHide={() => {
|
966 |
+
setSetting(false);
|
967 |
+
}}
|
968 |
+
/>
|
969 |
+
</div>
|
970 |
+
</Container>
|
971 |
+
</Navbar>
|
972 |
+
</header>
|
973 |
+
<Container fluid>
|
974 |
+
<Row style={{ minHeight: "100vh" }}>
|
975 |
+
<Col
|
976 |
+
md="2"
|
977 |
+
lg="2"
|
978 |
+
xl="2"
|
979 |
+
className="ps-0 d-none d-md-block"
|
980 |
+
>
|
981 |
+
<Offcanvas
|
982 |
+
className="leftsidebar h-100 bg-light"
|
983 |
+
show={showSideBar}
|
984 |
+
onHide={handleSidebarClose}
|
985 |
+
placement="start"
|
986 |
+
responsive="md"
|
987 |
+
>
|
988 |
+
<Offcanvas.Header
|
989 |
+
className="py-2 border-bottom"
|
990 |
+
closeButton
|
991 |
+
>
|
992 |
+
<Offcanvas.Title>
|
993 |
+
离线任务
|
994 |
+
</Offcanvas.Title>
|
995 |
+
</Offcanvas.Header>
|
996 |
+
<Offcanvas.Body className="p-0">
|
997 |
+
<Container fluid className="p-0">
|
998 |
+
<Nav
|
999 |
+
activeKey="1"
|
1000 |
+
className="flex-column"
|
1001 |
+
>
|
1002 |
+
<Nav.Link
|
1003 |
+
as={Link}
|
1004 |
+
className="nav-link text-dark"
|
1005 |
+
to="/"
|
1006 |
+
onClick={
|
1007 |
+
handleSidebarClose
|
1008 |
+
}
|
1009 |
+
>
|
1010 |
+
<Icon
|
1011 |
+
icon="plus"
|
1012 |
+
size="6"
|
1013 |
+
className="me-2"
|
1014 |
+
/>
|
1015 |
+
收藏
|
1016 |
+
</Nav.Link>
|
1017 |
+
<Nav.Link
|
1018 |
+
as={Link}
|
1019 |
+
className="nav-link text-dark"
|
1020 |
+
to="/videos"
|
1021 |
+
onClick={
|
1022 |
+
handleSidebarClose
|
1023 |
+
}
|
1024 |
+
>
|
1025 |
+
<Icon
|
1026 |
+
icon="movie"
|
1027 |
+
size="6"
|
1028 |
+
className="me-2"
|
1029 |
+
/>
|
1030 |
+
视频
|
1031 |
+
</Nav.Link>
|
1032 |
+
</Nav>
|
1033 |
+
</Container>
|
1034 |
+
</Offcanvas.Body>
|
1035 |
+
</Offcanvas>
|
1036 |
+
</Col>
|
1037 |
+
|
1038 |
+
<Col xs="12" sm="12" md="10" lg="10" xl="10">
|
1039 |
+
<main>
|
1040 |
+
<Container fluid className="pt-2 px-0 pb-5">
|
1041 |
+
{children}
|
1042 |
+
</Container>
|
1043 |
+
</main>
|
1044 |
+
</Col>
|
1045 |
+
</Row>
|
1046 |
+
</Container>
|
1047 |
+
</div>
|
1048 |
+
);
|
1049 |
+
};
|
1050 |
+
const Home = () => {
|
1051 |
+
const location = useLocation();
|
1052 |
+
const { id } = useParams();
|
1053 |
+
return (
|
1054 |
+
<div>
|
1055 |
+
<div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
1056 |
+
<label className="fs-3">Home</label>
|
1057 |
+
<ButtonToolbar
|
1058 |
+
aria-label="文件列表"
|
1059 |
+
className="bg-teal rounded"
|
1060 |
+
>
|
1061 |
+
<ButtonGroup className="bg-teal">
|
1062 |
+
<IconButton
|
1063 |
+
onClick={() => {
|
1064 |
+
alert("test")
|
1065 |
+
}}
|
1066 |
+
text="刷新"
|
1067 |
+
className="bg-teal border-0"
|
1068 |
+
icon="reload"
|
1069 |
+
iconClassName="me-1 text-white"
|
1070 |
+
iconSize="6"
|
1071 |
+
/>
|
1072 |
+
<IconButton
|
1073 |
+
onClick={() => {
|
1074 |
+
alert("hello");
|
1075 |
+
}}
|
1076 |
+
text="删除"
|
1077 |
+
className="bg-teal border-0"
|
1078 |
+
icon="delete-outline"
|
1079 |
+
iconClassName="me-1 text-white"
|
1080 |
+
iconSize="6"
|
1081 |
+
/>
|
1082 |
+
</ButtonGroup>
|
1083 |
+
</ButtonToolbar>
|
1084 |
+
</div>
|
1085 |
+
<Container fluid className="p-2"></Container>
|
1086 |
+
</div>
|
1087 |
+
);
|
1088 |
+
};
|
1089 |
+
|
1090 |
+
const Videos = () => {
|
1091 |
+
const [reload, setReload] = useState(false);
|
1092 |
+
const { limit, onPaginationChange, skip, pagination } = usePagination();
|
1093 |
+
const [meta, setMeta] = useState({ filter_count: 0 })
|
1094 |
+
const [keyword, setKeyword] = useState("")
|
1095 |
+
const [search, setSearch] = useState("")
|
1096 |
+
const [videos, setVideos] = useState([])
|
1097 |
+
const { data: linksData, refetch: linksRefetch, isLoading: linksLoading, error: linksError } = useQuery({
|
1098 |
+
queryKey: ['get_paginate_links', limit, pagination.pageIndex, search],
|
1099 |
+
queryFn: () => paginateLinksGet(limit, pagination.pageIndex, search),
|
1100 |
+
enabled: false,
|
1101 |
+
})
|
1102 |
+
useEffect(() => {
|
1103 |
+
linksRefetch()
|
1104 |
+
}, [pagination, reload, search]);
|
1105 |
+
useEffect(() => {
|
1106 |
+
linksRefetch()
|
1107 |
+
}, []);
|
1108 |
+
useEffect(() => {
|
1109 |
+
if (linksData) {
|
1110 |
+
setMeta({ filter_count: linksData.total })
|
1111 |
+
setVideos([...linksData.items])
|
1112 |
+
}
|
1113 |
+
}, [linksData]);
|
1114 |
+
|
1115 |
+
const handleSearchClick = () => {
|
1116 |
+
setSearch(keyword)
|
1117 |
+
onPaginationChange({ pageSize: 36, pageIndex: 1 })
|
1118 |
+
};
|
1119 |
+
|
1120 |
+
|
1121 |
+
|
1122 |
+
const forceUpdate = () => {
|
1123 |
+
setReload((pre) => !pre);
|
1124 |
+
};
|
1125 |
+
|
1126 |
+
return (
|
1127 |
+
<div>
|
1128 |
+
<div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
1129 |
+
<label className="fs-3">视频列表</label>
|
1130 |
+
<ButtonToolbar
|
1131 |
+
aria-label="视频列表"
|
1132 |
+
className="bg-teal rounded"
|
1133 |
+
>
|
1134 |
+
<ButtonGroup className="bg-teal">
|
1135 |
+
<IconButton
|
1136 |
+
onClick={() => {
|
1137 |
+
forceUpdate();
|
1138 |
+
}}
|
1139 |
+
text="刷新"
|
1140 |
+
className="bg-teal border-0"
|
1141 |
+
icon="reload"
|
1142 |
+
iconClassName="me-1 text-white"
|
1143 |
+
iconSize="6"
|
1144 |
+
/>
|
1145 |
+
</ButtonGroup>
|
1146 |
+
</ButtonToolbar>
|
1147 |
+
</div>
|
1148 |
+
{linksError && (
|
1149 |
+
<div className="text-center text-danger">
|
1150 |
+
发生错误,请稍后重试!!!
|
1151 |
+
</div>
|
1152 |
+
)}
|
1153 |
+
|
1154 |
+
<Container fluid className="p-2">
|
1155 |
+
<InputGroup className="mb-3">
|
1156 |
+
<Form.Control
|
1157 |
+
placeholder="关键词"
|
1158 |
+
aria-label="关键词"
|
1159 |
+
aria-describedby="关键词"
|
1160 |
+
onChange={e => setKeyword(e.target.value)}
|
1161 |
+
/>
|
1162 |
+
<Button variant="outline-secondary" id="button-addon2" onClick={() => { handleSearchClick() }}>
|
1163 |
+
搜索
|
1164 |
+
</Button>
|
1165 |
+
</InputGroup>
|
1166 |
+
|
1167 |
+
<Row>
|
1168 |
+
<Col xs={12} className="py-2">
|
1169 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1170 |
+
</Col>
|
1171 |
+
</Row>
|
1172 |
+
{(linksLoading) && (
|
1173 |
+
<Row>
|
1174 |
+
<Col xs={12} className="py-2">
|
1175 |
+
<div className="text-center text-success">
|
1176 |
+
正在努力加载中......
|
1177 |
+
</div>
|
1178 |
+
</Col>
|
1179 |
+
</Row>
|
1180 |
+
)}
|
1181 |
+
{linksData && (
|
1182 |
+
<Row>
|
1183 |
+
{videos.map((video, index) => (
|
1184 |
+
<Col xs={12} md={3} className="py-2">
|
1185 |
+
<Video video={video} />
|
1186 |
+
</Col>
|
1187 |
+
))}
|
1188 |
+
</Row>
|
1189 |
+
)}
|
1190 |
+
|
1191 |
+
|
1192 |
+
|
1193 |
+
|
1194 |
+
<Row>
|
1195 |
+
<Col xs={12} className="py-2">
|
1196 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1197 |
+
</Col>
|
1198 |
+
</Row>
|
1199 |
+
</Container>
|
1200 |
+
</div>
|
1201 |
+
);
|
1202 |
+
};
|
1203 |
+
|
1204 |
+
|
1205 |
+
const Favorites = () => {
|
1206 |
+
const [reload, setReload] = useState(false);
|
1207 |
+
const { limit, onPaginationChange, skip, pagination } = usePagination();
|
1208 |
+
const [meta, setMeta] = useState({ filter_count: 0 })
|
1209 |
+
const [keyword, setKeyword] = useState("")
|
1210 |
+
const [search, setSearch] = useState("")
|
1211 |
+
const [videos, setVideos] = useState([])
|
1212 |
+
const { data: linksData, refetch: linksRefetch, isLoading: linksLoading, error: linksError } = useQuery({
|
1213 |
+
queryKey: ['get_paginate_favorites', limit, pagination.pageIndex, search],
|
1214 |
+
queryFn: () => paginateFavoritesGet(limit, pagination.pageIndex, search),
|
1215 |
+
enabled: false,
|
1216 |
+
})
|
1217 |
+
useEffect(() => {
|
1218 |
+
linksRefetch()
|
1219 |
+
}, [pagination, reload, search]);
|
1220 |
+
useEffect(() => {
|
1221 |
+
linksRefetch()
|
1222 |
+
}, []);
|
1223 |
+
useEffect(() => {
|
1224 |
+
if (linksData) {
|
1225 |
+
setMeta({ filter_count: linksData.total })
|
1226 |
+
setVideos([...linksData.items])
|
1227 |
+
}
|
1228 |
+
}, [linksData]);
|
1229 |
+
|
1230 |
+
const handleSearchClick = () => {
|
1231 |
+
setSearch(keyword)
|
1232 |
+
onPaginationChange({ pageSize: 36, pageIndex: 1 })
|
1233 |
+
};
|
1234 |
+
|
1235 |
+
|
1236 |
+
|
1237 |
+
const forceUpdate = () => {
|
1238 |
+
setReload((pre) => !pre);
|
1239 |
+
};
|
1240 |
+
|
1241 |
+
return (
|
1242 |
+
<div>
|
1243 |
+
<div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
1244 |
+
<label className="fs-3">收藏列表</label>
|
1245 |
+
<ButtonToolbar
|
1246 |
+
aria-label="收藏列表"
|
1247 |
+
className="bg-teal rounded"
|
1248 |
+
>
|
1249 |
+
<ButtonGroup className="bg-teal">
|
1250 |
+
<IconButton
|
1251 |
+
onClick={() => {
|
1252 |
+
forceUpdate();
|
1253 |
+
}}
|
1254 |
+
text="刷新"
|
1255 |
+
className="bg-teal border-0"
|
1256 |
+
icon="reload"
|
1257 |
+
iconClassName="me-1 text-white"
|
1258 |
+
iconSize="6"
|
1259 |
+
/>
|
1260 |
+
</ButtonGroup>
|
1261 |
+
</ButtonToolbar>
|
1262 |
+
</div>
|
1263 |
+
{linksError && (
|
1264 |
+
<div className="text-center text-danger">
|
1265 |
+
发生错误,请稍后重试!!!
|
1266 |
+
</div>
|
1267 |
+
)}
|
1268 |
+
|
1269 |
+
<Container fluid className="p-2">
|
1270 |
+
<InputGroup className="mb-3">
|
1271 |
+
<Form.Control
|
1272 |
+
placeholder="关键词"
|
1273 |
+
aria-label="关键词"
|
1274 |
+
aria-describedby="关键词"
|
1275 |
+
onChange={e => setKeyword(e.target.value)}
|
1276 |
+
/>
|
1277 |
+
<Button variant="outline-secondary" id="button-addon2" onClick={() => { handleSearchClick() }}>
|
1278 |
+
搜索
|
1279 |
+
</Button>
|
1280 |
+
</InputGroup>
|
1281 |
+
|
1282 |
+
<Row>
|
1283 |
+
<Col xs={12} className="py-2">
|
1284 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1285 |
+
</Col>
|
1286 |
+
</Row>
|
1287 |
+
{(linksLoading) && (
|
1288 |
+
<Row>
|
1289 |
+
<Col xs={12} className="py-2">
|
1290 |
+
<div className="text-center text-success">
|
1291 |
+
正在努力加载中......
|
1292 |
+
</div>
|
1293 |
+
</Col>
|
1294 |
+
</Row>
|
1295 |
+
)}
|
1296 |
+
{linksData && (
|
1297 |
+
<Row>
|
1298 |
+
{videos.map((video, index) => (
|
1299 |
+
<Col xs={12} md={3} className="py-2">
|
1300 |
+
<Video video={video} />
|
1301 |
+
</Col>
|
1302 |
+
))}
|
1303 |
+
</Row>
|
1304 |
+
)}
|
1305 |
+
|
1306 |
+
|
1307 |
+
|
1308 |
+
|
1309 |
+
<Row>
|
1310 |
+
<Col xs={12} className="py-2">
|
1311 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1312 |
+
</Col>
|
1313 |
+
</Row>
|
1314 |
+
</Container>
|
1315 |
+
</div>
|
1316 |
+
);
|
1317 |
+
};
|
1318 |
+
|
1319 |
+
|
1320 |
+
const Tags = () => {
|
1321 |
+
const { tag } = useParams()
|
1322 |
+
const [reload, setReload] = useState(false);
|
1323 |
+
const { limit, onPaginationChange, skip, pagination } = usePagination();
|
1324 |
+
const [meta, setMeta] = useState({ filter_count: 0 })
|
1325 |
+
const [keyword, setKeyword] = useState("")
|
1326 |
+
const [videos, setVideos] = useState([])
|
1327 |
+
const { data: linksData, refetch: linksRefetch, isLoading: linksLoading, error: linksError } = useQuery({
|
1328 |
+
queryKey: ['get_tag_links', limit, pagination.pageIndex],
|
1329 |
+
queryFn: () => paginateTagLinksGet(limit, pagination.pageIndex, tag),
|
1330 |
+
enabled: false,
|
1331 |
+
})
|
1332 |
+
useEffect(() => {
|
1333 |
+
linksRefetch()
|
1334 |
+
}, [pagination, reload]);
|
1335 |
+
useEffect(() => {
|
1336 |
+
linksRefetch()
|
1337 |
+
}, []);
|
1338 |
+
useEffect(() => {
|
1339 |
+
if (linksData) {
|
1340 |
+
setMeta({ filter_count: linksData.total })
|
1341 |
+
setVideos([...linksData.items])
|
1342 |
+
}
|
1343 |
+
}, [linksData]);
|
1344 |
+
|
1345 |
+
|
1346 |
+
const forceUpdate = () => {
|
1347 |
+
setReload((pre) => !pre);
|
1348 |
+
};
|
1349 |
+
|
1350 |
+
return (
|
1351 |
+
<div>
|
1352 |
+
<div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
1353 |
+
<label className="fs-3">视频列表</label>
|
1354 |
+
<ButtonToolbar
|
1355 |
+
aria-label="视频列表"
|
1356 |
+
className="bg-teal rounded"
|
1357 |
+
>
|
1358 |
+
<ButtonGroup className="bg-teal">
|
1359 |
+
<IconButton
|
1360 |
+
onClick={() => {
|
1361 |
+
forceUpdate();
|
1362 |
+
}}
|
1363 |
+
text="刷新"
|
1364 |
+
className="bg-teal border-0"
|
1365 |
+
icon="reload"
|
1366 |
+
iconClassName="me-1 text-white"
|
1367 |
+
iconSize="6"
|
1368 |
+
/>
|
1369 |
+
</ButtonGroup>
|
1370 |
+
</ButtonToolbar>
|
1371 |
+
</div>
|
1372 |
+
{linksError && (
|
1373 |
+
<div className="text-center text-danger">
|
1374 |
+
发生错误,请稍后重试!!!
|
1375 |
+
</div>
|
1376 |
+
)}
|
1377 |
+
|
1378 |
+
<Container fluid className="p-2">
|
1379 |
+
<Row>
|
1380 |
+
<Col xs={12} className="py-2">
|
1381 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1382 |
+
</Col>
|
1383 |
+
</Row>
|
1384 |
+
{(linksLoading) && (
|
1385 |
+
<Row>
|
1386 |
+
<Col xs={12} className="py-2">
|
1387 |
+
<div className="text-center text-success">
|
1388 |
+
正在努力加载中......
|
1389 |
+
</div>
|
1390 |
+
</Col>
|
1391 |
+
</Row>
|
1392 |
+
)}
|
1393 |
+
{linksData && (
|
1394 |
+
<Row>
|
1395 |
+
{videos.map((video, index) => (
|
1396 |
+
<Col xs={12} md={3} className="py-2">
|
1397 |
+
<Video video={video} />
|
1398 |
+
</Col>
|
1399 |
+
))}
|
1400 |
+
</Row>
|
1401 |
+
)}
|
1402 |
+
|
1403 |
+
<Row>
|
1404 |
+
<Col xs={12} className="py-2">
|
1405 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1406 |
+
</Col>
|
1407 |
+
</Row>
|
1408 |
+
</Container>
|
1409 |
+
</div>
|
1410 |
+
);
|
1411 |
+
};
|
1412 |
+
|
1413 |
+
|
1414 |
+
const Tasks = () => {
|
1415 |
+
const [show, setShow] = useState(false);
|
1416 |
+
const handleClose = () => setShow(false);
|
1417 |
+
const handleShow = () => setShow(true);
|
1418 |
+
const [reload, setReload] = useState(false);
|
1419 |
+
const { limit, onPaginationChange, skip, pagination } = usePagination();
|
1420 |
+
const [meta, setMeta] = useState({ filter_count: 0 })
|
1421 |
+
const [tasks, setTasks] = useState([])
|
1422 |
+
const { data: tasksData, refetch: tasksRefetch, isLoading: tasksLoading, error: tasksError } = useQuery({
|
1423 |
+
queryKey: ['get_paginate_tasks', limit, skip],
|
1424 |
+
queryFn: () => paginateTasksGet(limit, skip),
|
1425 |
+
enabled: show,
|
1426 |
+
})
|
1427 |
+
|
1428 |
+
useEffect(() => {
|
1429 |
+
//tasksRefetch()
|
1430 |
+
}, [pagination, reload]);
|
1431 |
+
|
1432 |
+
useEffect(() => {
|
1433 |
+
if (tasksData) {
|
1434 |
+
setMeta(tasksData.meta)
|
1435 |
+
setTasks([...tasksData.data])
|
1436 |
+
}
|
1437 |
+
}, [tasksData]);
|
1438 |
+
|
1439 |
+
|
1440 |
+
const forceUpdate = () => {
|
1441 |
+
setReload((pre) => !pre);
|
1442 |
+
};
|
1443 |
+
|
1444 |
+
return (
|
1445 |
+
<div>
|
1446 |
+
<Button
|
1447 |
+
style={{
|
1448 |
+
backgroundColor: "transparent",
|
1449 |
+
}}
|
1450 |
+
className="nav-link btn"
|
1451 |
+
onClick={handleShow}
|
1452 |
+
children={
|
1453 |
+
<span>
|
1454 |
+
<Icon
|
1455 |
+
icon="cloud-download-outline"
|
1456 |
+
size="3"
|
1457 |
+
className="text-white"
|
1458 |
+
/>
|
1459 |
+
</span>
|
1460 |
+
}
|
1461 |
+
></Button>
|
1462 |
+
|
1463 |
+
|
1464 |
+
<Modal show={show} onHide={handleClose}>
|
1465 |
+
<Modal.Header closeButton>
|
1466 |
+
<Modal.Title>远程下载任务</Modal.Title>
|
1467 |
+
</Modal.Header>
|
1468 |
+
<Modal.Body className="py-0">
|
1469 |
+
{tasksError && (
|
1470 |
+
<div className="text-center text-danger">
|
1471 |
+
发生错误,请稍后重试!!!
|
1472 |
+
</div>
|
1473 |
+
)}
|
1474 |
+
{(tasksLoading) && (
|
1475 |
+
<div className="text-center text-success">
|
1476 |
+
正在努力加载中......
|
1477 |
+
</div>
|
1478 |
+
)}
|
1479 |
+
<Container fluid className="p-2">
|
1480 |
+
<Row>
|
1481 |
+
<Col xs={12}>
|
1482 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1483 |
+
</Col>
|
1484 |
+
</Row>
|
1485 |
+
|
1486 |
+
<Row>
|
1487 |
+
<Col xs={12}>
|
1488 |
+
<Table bordered hover>
|
1489 |
+
<thead>
|
1490 |
+
<tr>
|
1491 |
+
<th>#</th>
|
1492 |
+
<th>文件名</th>
|
1493 |
+
<th>状态</th>
|
1494 |
+
</tr>
|
1495 |
+
</thead>
|
1496 |
+
{tasksData && (
|
1497 |
+
<tbody>
|
1498 |
+
{tasks.map((task, index) => (
|
1499 |
+
<tr>
|
1500 |
+
<td>{task.id}</td>
|
1501 |
+
<td>{task.url.substr(task.url.indexOf('##') + 2)}</td>
|
1502 |
+
<td>{task.status == 'draft' ? <span className="text-warning">待下载</span> : <span class="text-success">正在下载中</span>}</td>
|
1503 |
+
</tr>
|
1504 |
+
))}
|
1505 |
+
</tbody>
|
1506 |
+
)}
|
1507 |
+
|
1508 |
+
</Table>
|
1509 |
+
|
1510 |
+
</Col>
|
1511 |
+
</Row>
|
1512 |
+
|
1513 |
+
<Row>
|
1514 |
+
<Col xs={12} className="py-2">
|
1515 |
+
<Paginate page={pagination.pageIndex} onClick={(i) => { onPaginationChange({ pageSize: 36, pageIndex: i }) }} itemsPerPage="36" totalCount={meta.filter_count} />
|
1516 |
+
</Col>
|
1517 |
+
</Row>
|
1518 |
+
</Container>
|
1519 |
+
</Modal.Body>
|
1520 |
+
<Modal.Footer className="justify-content-between">
|
1521 |
+
<Button variant="primary" onClick={() => { forceUpdate(); }}>
|
1522 |
+
刷新
|
1523 |
+
</Button>
|
1524 |
+
<Button variant="primary" onClick={() => {
|
1525 |
+
const setting = STORE.getState().settings;
|
1526 |
+
showLoading();
|
1527 |
+
axios.post(setting.github_host, { "ref": "main", "inputs": {} }, {
|
1528 |
+
headers: {
|
1529 |
+
'Authorization': "Bearer " + setting.github_token,
|
1530 |
+
'Accept': 'application/vnd.github+json',
|
1531 |
+
'X-GitHub-Api-Version': '2022-11-28',
|
1532 |
+
},
|
1533 |
+
}).then(function (response) {
|
1534 |
+
layer.msg('任务启动成功', { time: 2000, icon: 6 });
|
1535 |
+
//console.log(response);
|
1536 |
+
})
|
1537 |
+
.catch(function (error) {
|
1538 |
+
console.log(error);
|
1539 |
+
}).finally(() => {
|
1540 |
+
hideLoading();
|
1541 |
+
});
|
1542 |
+
}}>
|
1543 |
+
开始下载
|
1544 |
+
</Button>
|
1545 |
+
<Button variant="primary" onClick={handleClose}>
|
1546 |
+
关闭
|
1547 |
+
</Button>
|
1548 |
+
</Modal.Footer>
|
1549 |
+
</Modal>
|
1550 |
+
|
1551 |
+
</div >);
|
1552 |
+
};
|
1553 |
+
|
1554 |
+
|
1555 |
+
|
1556 |
+
|
1557 |
+
|
1558 |
+
|
1559 |
+
|
1560 |
+
|
1561 |
+
const LocalTasks = () => {
|
1562 |
+
const [show, setShow] = useState(false);
|
1563 |
+
const handleClose = () => setShow(false);
|
1564 |
+
const handleShow = () => setShow(true);
|
1565 |
+
const [downloads, setDownloads] = useState([])
|
1566 |
+
const [addDownloadObject, setAddDownloadObject] = useState({})
|
1567 |
+
const setting = STORE.getState().settings;
|
1568 |
+
const columns = [
|
1569 |
+
{ title: "文件名称", dataIndex: "name" },
|
1570 |
+
{ title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) },
|
1571 |
+
{ title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) },
|
1572 |
+
{
|
1573 |
+
title: "操作",
|
1574 |
+
dataIndex: "name",
|
1575 |
+
render: (row) => (
|
1576 |
+
<div>
|
1577 |
+
<Icon
|
1578 |
+
icon="delete-outline"
|
1579 |
+
size="6"
|
1580 |
+
className="me-2"
|
1581 |
+
onClick={() => {
|
1582 |
+
layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) {
|
1583 |
+
setDownloads(
|
1584 |
+
downloads.filter(a =>
|
1585 |
+
a.name !== row.name
|
1586 |
+
)
|
1587 |
+
);
|
1588 |
+
layer.close(index);
|
1589 |
+
}, function () {
|
1590 |
+
|
1591 |
+
});
|
1592 |
+
}}
|
1593 |
+
/>
|
1594 |
+
<Icon
|
1595 |
+
icon="pencil-outline"
|
1596 |
+
size="6"
|
1597 |
+
className="me-2"
|
1598 |
+
onClick={() => {
|
1599 |
+
layer.prompt({
|
1600 |
+
title: '输入文件名称,并确认',
|
1601 |
+
formType: 0,
|
1602 |
+
value: row.name,
|
1603 |
+
success: function (layero, index) {
|
1604 |
+
$(".layui-layer").eq(0).css("top", "0px");
|
1605 |
+
$("div[aria-modal]").eq(0).removeAttr("tabindex");//解决弹出窗的input无法获取焦点的问题
|
1606 |
+
},
|
1607 |
+
end: function (layero, index) {
|
1608 |
+
$("div[aria-modal]").eq(0).attr("tabindex", -1).focus();//再把焦点还回去
|
1609 |
+
}
|
1610 |
+
}, function (value, index) {
|
1611 |
+
const newDownloads = downloads.map(downloadItem => {
|
1612 |
+
if (downloadItem.name === row.name) {
|
1613 |
+
return {
|
1614 |
+
...downloadItem,
|
1615 |
+
name: value
|
1616 |
+
};
|
1617 |
+
}
|
1618 |
+
return downloadItem;
|
1619 |
+
});
|
1620 |
+
setDownloads(newDownloads);
|
1621 |
+
layer.close(index);
|
1622 |
+
});
|
1623 |
+
}}
|
1624 |
+
/>
|
1625 |
+
</div>
|
1626 |
+
),
|
1627 |
+
},
|
1628 |
+
];
|
1629 |
+
|
1630 |
+
|
1631 |
+
const { mutateAsync: localTaskdMutation } = useMutation({
|
1632 |
+
mutationKey: ["get-download"],
|
1633 |
+
mutationFn: async () => {
|
1634 |
+
showLoading();
|
1635 |
+
var host = setting.directus_host;
|
1636 |
+
if (!host.endsWith("/")) {
|
1637 |
+
host = host + '/'
|
1638 |
+
}
|
1639 |
+
var url = host + 'items/task';
|
1640 |
+
const tasks = downloads.map(task => {
|
1641 |
+
return { url: task.url + '##' + task.name }
|
1642 |
+
})
|
1643 |
+
return await axios.post(url, tasks, {
|
1644 |
+
headers: {
|
1645 |
+
'Authorization': "Bearer " + setting.directus_token,
|
1646 |
+
'Content-Type': 'application/json'
|
1647 |
+
},
|
1648 |
+
})
|
1649 |
+
},
|
1650 |
+
onSuccess: async (data, variables, context) => {
|
1651 |
+
hideLoading();
|
1652 |
+
layer.msg('任务添加成功', { time: 2000, icon: 6 });
|
1653 |
+
},
|
1654 |
+
onError: () => {
|
1655 |
+
hideLoading();
|
1656 |
+
layer.msg('任务添加失败', { time: 2000, icon: 5 });
|
1657 |
+
}
|
1658 |
+
})
|
1659 |
+
|
1660 |
+
|
1661 |
+
|
1662 |
+
const addDowload = (fileinfo) => {
|
1663 |
+
const file = fileinfo.data.data;
|
1664 |
+
const download = { name: file.name, size: file.size, url: file.raw_url, created: file.created }
|
1665 |
+
setAddDownloadObject(download)
|
1666 |
+
}
|
1667 |
+
useEffect(() => {
|
1668 |
+
if (addDownloadObject && ('name' in addDownloadObject)) {
|
1669 |
+
setDownloads([...downloads, addDownloadObject])
|
1670 |
+
setAddDownloadObject({})
|
1671 |
+
}
|
1672 |
+
}, [addDownloadObject]);
|
1673 |
+
useEffect(() => {
|
1674 |
+
onEvent("addDownload", addDowload)
|
1675 |
+
settingStorage.getItem('downloads').then(function (value) {
|
1676 |
+
if (value) {
|
1677 |
+
setDownloads(value)
|
1678 |
+
}
|
1679 |
+
}).catch(function (err) {
|
1680 |
+
console.log(err)
|
1681 |
+
});
|
1682 |
+
}, []);
|
1683 |
+
useEffect(() => {
|
1684 |
+
settingStorage.setItem('downloads', downloads)
|
1685 |
+
}, [downloads]);
|
1686 |
+
|
1687 |
+
|
1688 |
+
if (downloads.length > 0) {
|
1689 |
+
return (
|
1690 |
+
<div>
|
1691 |
+
<Button
|
1692 |
+
style={{
|
1693 |
+
backgroundColor: "transparent",
|
1694 |
+
}}
|
1695 |
+
className="nav-link btn"
|
1696 |
+
onClick={handleShow}
|
1697 |
+
children={
|
1698 |
+
<span>
|
1699 |
+
<Icon
|
1700 |
+
icon="download"
|
1701 |
+
size="3"
|
1702 |
+
className="text-white"
|
1703 |
+
/>
|
1704 |
+
<Badge bg="danger" style={{ top: '-15px', left: '-10px' }}>{downloads.length}</Badge>
|
1705 |
+
</span>
|
1706 |
+
}
|
1707 |
+
></Button>
|
1708 |
+
|
1709 |
+
|
1710 |
+
<Modal show={show} onHide={handleClose}>
|
1711 |
+
<Modal.Header closeButton>
|
1712 |
+
<Modal.Title>本地下载任务</Modal.Title>
|
1713 |
+
</Modal.Header>
|
1714 |
+
<Modal.Body>
|
1715 |
+
{downloads && (
|
1716 |
+
<DataTable data={downloads ? downloads : []} columns={columns} />
|
1717 |
+
)}
|
1718 |
+
</Modal.Body>
|
1719 |
+
<Modal.Footer className="justify-content-between">
|
1720 |
+
|
1721 |
+
<ButtonGroup>
|
1722 |
+
<Button variant="primary" onClick={async () => { await localTaskdMutation() }}>
|
1723 |
+
添加转存
|
1724 |
+
</Button>
|
1725 |
+
</ButtonGroup>
|
1726 |
+
|
1727 |
+
<ButtonGroup>
|
1728 |
+
<Button variant="danger" onClick={() => {
|
1729 |
+
layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) {
|
1730 |
+
setDownloads([]);
|
1731 |
+
layer.close(index);
|
1732 |
+
}, function () {
|
1733 |
+
|
1734 |
+
});
|
1735 |
+
}}>
|
1736 |
+
清空
|
1737 |
+
</Button>
|
1738 |
+
<Button variant="primary" onClick={handleClose}>
|
1739 |
+
关闭
|
1740 |
+
</Button>
|
1741 |
+
</ButtonGroup>
|
1742 |
+
|
1743 |
+
|
1744 |
+
</Modal.Footer>
|
1745 |
+
</Modal>
|
1746 |
+
|
1747 |
+
</div >
|
1748 |
+
);
|
1749 |
+
}
|
1750 |
+
}
|
1751 |
+
|
1752 |
+
|
1753 |
+
App = () => {
|
1754 |
+
const [open, setOpen] = useState(false);
|
1755 |
+
const [reload, setReload] = useState(false);
|
1756 |
+
const [response, error, loading, fetchDataByPage] = getFiles();
|
1757 |
+
const { folder } = useParams();
|
1758 |
+
const location = useLocation();
|
1759 |
+
const [path, setPath] = useState(decodeURI(location.pathname));
|
1760 |
+
const [page, setPage] = useState(1);
|
1761 |
+
const [query, setQuery] = useState({ "path": path, "password": "", "page": page, "per_page": 0, "refresh": true });
|
1762 |
+
const setting = STORE.getState().settings;
|
1763 |
+
|
1764 |
+
|
1765 |
+
//const queryClient = useQueryClient()
|
1766 |
+
// Queries
|
1767 |
+
//const { data, error, isLoading, refetch } = useQuery({
|
1768 |
+
// queryKey: ['test'], queryFn: () => axios.get("")
|
1769 |
+
//})
|
1770 |
+
|
1771 |
+
const { data: fileData, mutateAsync: downloadMutation } = useMutation({
|
1772 |
+
mutationKey: ["get-download"],
|
1773 |
+
mutationFn: async (fileinfo) => {
|
1774 |
+
showLoading();
|
1775 |
+
var host = setting.alist_host;
|
1776 |
+
if (!host.endsWith("/")) {
|
1777 |
+
host = host + '/'
|
1778 |
+
}
|
1779 |
+
var url = host + 'api/fs/get';
|
1780 |
+
return await axios.post(url, fileinfo, {
|
1781 |
+
headers: {
|
1782 |
+
'Authorization': setting.alist_token,
|
1783 |
+
'Content-Type': 'application/json'
|
1784 |
+
},
|
1785 |
+
})
|
1786 |
+
},
|
1787 |
+
onSuccess: async (data, variables, context) => {
|
1788 |
+
hideLoading();
|
1789 |
+
},
|
1790 |
+
onError: () => {
|
1791 |
+
hideLoading();
|
1792 |
+
}
|
1793 |
+
})
|
1794 |
+
|
1795 |
+
useEffect(() => {
|
1796 |
+
if (fileData) {
|
1797 |
+
emitEvent("addDownload", fileData)
|
1798 |
+
}
|
1799 |
+
}, [fileData]);
|
1800 |
+
|
1801 |
+
|
1802 |
+
const columns = [
|
1803 |
+
{ title: "文件名称", dataIndex: "name" },
|
1804 |
+
{ title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) },
|
1805 |
+
{ title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) },
|
1806 |
+
{
|
1807 |
+
title: "操作",
|
1808 |
+
dataIndex: "name",
|
1809 |
+
render: (row) => (
|
1810 |
+
row.is_dir ? <Nav.Link
|
1811 |
+
as={Link}
|
1812 |
+
className="nav-link text-dark"
|
1813 |
+
to={decodeURI(path + row.name + '/')}
|
1814 |
+
target="_blank"
|
1815 |
+
>
|
1816 |
+
<Icon
|
1817 |
+
icon="open-in-new"
|
1818 |
+
size="6"
|
1819 |
+
className="me-2"
|
1820 |
+
/>
|
1821 |
+
</Nav.Link> :
|
1822 |
+
<Icon
|
1823 |
+
icon="download-outline"
|
1824 |
+
size="6"
|
1825 |
+
className="me-2"
|
1826 |
+
onClick={async () => {
|
1827 |
+
let data = { "path": path + row.name, "password": "" }
|
1828 |
+
await downloadMutation(data);
|
1829 |
+
}}
|
1830 |
+
/>
|
1831 |
+
),
|
1832 |
+
},
|
1833 |
+
];
|
1834 |
+
useEffect(() => {
|
1835 |
+
if (!setting.alist_token || setting.alist_token.length < 5) {
|
1836 |
+
layer.alert("请先正确配置Alsit的令牌", { icon: 5 });
|
1837 |
+
return
|
1838 |
+
}
|
1839 |
+
fetchDataByPage(setting, query);
|
1840 |
+
return () => { }
|
1841 |
+
}, [reload, query]);
|
1842 |
+
|
1843 |
+
|
1844 |
+
const forceUpdate = () => {
|
1845 |
+
setReload((pre) => !pre);
|
1846 |
+
};
|
1847 |
+
|
1848 |
+
return (
|
1849 |
+
<div>
|
1850 |
+
<div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
1851 |
+
<label className="fs-3">文件列表</label>
|
1852 |
+
<ButtonToolbar
|
1853 |
+
aria-label="功能区"
|
1854 |
+
className="bg-teal rounded"
|
1855 |
+
>
|
1856 |
+
<ButtonGroup className="bg-teal">
|
1857 |
+
<IconButton
|
1858 |
+
onClick={() => {
|
1859 |
+
emitEvent("test", { a: 'b' })
|
1860 |
+
}}
|
1861 |
+
text="刷新"
|
1862 |
+
className="bg-teal border-0"
|
1863 |
+
icon="reload"
|
1864 |
+
iconClassName="me-1 text-white"
|
1865 |
+
iconSize="6"
|
1866 |
+
/>
|
1867 |
+
</ButtonGroup>
|
1868 |
+
</ButtonToolbar>
|
1869 |
+
</div>
|
1870 |
+
<Container fluid className="p-2">
|
1871 |
+
{error && (
|
1872 |
+
<div className="text-center text-danger">
|
1873 |
+
{error}
|
1874 |
+
</div>
|
1875 |
+
)}
|
1876 |
+
{(loading) && (
|
1877 |
+
<div className="text-center text-success">
|
1878 |
+
正在努力加载中......
|
1879 |
+
</div>
|
1880 |
+
)}
|
1881 |
+
{response && (
|
1882 |
+
<DataTable data={response.data.content ? response.data.content : []} columns={columns} />
|
1883 |
+
)}
|
1884 |
+
</Container>
|
1885 |
+
</div>
|
1886 |
+
);
|
1887 |
+
};
|
1888 |
+
|
1889 |
+
const container = document.getElementById("root");
|
1890 |
+
const root = ReactDOM.createRoot(container);
|
1891 |
+
root.render(
|
1892 |
+
<QueryClientProvider client={queryClient}>
|
1893 |
+
<HashRouter>
|
1894 |
+
<Route path="/:path?">
|
1895 |
+
<Layout>
|
1896 |
+
<Switch>
|
1897 |
+
<Route path="/videos" exact component={Videos} />
|
1898 |
+
<Route path="/tags/:tag?" exact component={Tags} />
|
1899 |
+
<Route path="/" exact component={Favorites} />
|
1900 |
+
</Switch>
|
1901 |
+
</Layout>
|
1902 |
+
</Route>
|
1903 |
+
</HashRouter>
|
1904 |
+
</QueryClientProvider>
|
1905 |
+
);
|
1906 |
+
|
1907 |
+
$(document).ready(function () {
|
1908 |
+
$(window).scroll(function () {
|
1909 |
+
if ($(this).scrollTop() > 50) {
|
1910 |
+
$("#back-to-top").fadeIn();
|
1911 |
+
} else {
|
1912 |
+
$("#back-to-top").fadeOut();
|
1913 |
+
}
|
1914 |
+
});
|
1915 |
+
// scroll body to 0px on click
|
1916 |
+
$("#back-to-top").click(function () {
|
1917 |
+
$("body,html").animate(
|
1918 |
+
{
|
1919 |
+
scrollTop: 0,
|
1920 |
+
},
|
1921 |
+
400
|
1922 |
+
);
|
1923 |
+
return false;
|
1924 |
+
});
|
1925 |
+
});
|
1926 |
+
</script>
|
1927 |
+
</body>
|
1928 |
+
|
1929 |
+
</html>
|