feat: enhance ADB path resolution and update HTTP request handling for improved flexibility
This commit is contained in:
@@ -12,6 +12,7 @@ dist/
|
|||||||
src-tauri/target/
|
src-tauri/target/
|
||||||
src-tauri/gen/schemas
|
src-tauri/gen/schemas
|
||||||
vendor/webview2/
|
vendor/webview2/
|
||||||
|
vendor/platform-tools/
|
||||||
server/.venv/
|
server/.venv/
|
||||||
download/
|
download/
|
||||||
tools/ffmpeg/
|
tools/ffmpeg/
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# RaptDrama Download Tools
|
||||||
|
|
||||||
|
Windows 桌面程序,用来登录 RaptDrama、浏览剧目、播放已缓存的分集,以及把剧集下载成 MP4。
|
||||||
|
|
||||||
|
## 使用分发包
|
||||||
|
|
||||||
|
1. 解压 `dist/RaptDrama-Download-Tools-windows-x64.zip`。
|
||||||
|
2. 进入 `RaptDrama-Download-Tools`,运行 `raptdrama.exe`。
|
||||||
|
3. 压缩包里已经带有 Python、adb、ffmpeg 和 WebView2 离线安装包。本机如果还没有 WebView2,程序启动时会静默安装。
|
||||||
|
|
||||||
|
程序会在自己的目录里写入:
|
||||||
|
|
||||||
|
- `download/`:下载下来的视频
|
||||||
|
- `cache/`:封面、剧目清单和分集播放地址
|
||||||
|
|
||||||
|
分集播放地址会留在缓存里。账号还能拿到新地址时会更新缓存;VIP 失效后,缓存里已经有地址的集仍然可以播放。签名过期的地址无法继续播放。
|
||||||
|
|
||||||
|
关闭窗口时,如果还有未完成的下载,会先询问。确定后才会退出。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
需要 Windows、Node.js、Rust 和 Python 3.13。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv server\.venv
|
||||||
|
server\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
|
||||||
|
npm install
|
||||||
|
npm install --prefix ui
|
||||||
|
npm run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
开发时界面和后台都从仓库目录读取。剧目清单默认使用 `cache/catalog.json`,正式打包会关掉这份缓存。封面缓存和分集地址缓存会保留。
|
||||||
|
|
||||||
|
## 打包
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\package.py
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会编译界面和桌面程序,并把本地已经放好的文件拷进压缩包:
|
||||||
|
|
||||||
|
- `dist/cache` 里的 Windows 嵌入式 Python,以及 `server/requirements.txt` 里的依赖
|
||||||
|
- `vendor/platform-tools` 里的 adb,拷到压缩包的 `tools/platform-tools`
|
||||||
|
- `tools/ffmpeg/ffmpeg.exe`
|
||||||
|
- `vendor/webview2/MicrosoftEdgeWebView2RuntimeInstallerX64.exe`
|
||||||
|
|
||||||
|
adb 和 WebView2 打包时不再下载。完成后得到 `dist/RaptDrama-Download-Tools-windows-x64.zip`。
|
||||||
|
|
||||||
|
打包脚本是 Python,公共步骤和平台相关的运行时是分开的。目前只接好了 Windows。macOS 和 Linux 以后把对应文件放进 `vendor` 即可。
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""打出可分发的压缩包。
|
||||||
|
|
||||||
|
在仓库根目录运行:
|
||||||
|
|
||||||
|
python scripts/package.py
|
||||||
|
|
||||||
|
Windows 会编译界面和桌面程序,并拷贝本地已有的 Python 缓存、adb、ffmpeg 和 WebView2 离线安装包。
|
||||||
|
macOS、Linux 以后可以在对应函数里补上自己的运行时,其余步骤可以复用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DIST = ROOT / "dist"
|
||||||
|
CACHE = DIST / "cache"
|
||||||
|
STAGE_NAME = "RaptDrama-Download-Tools"
|
||||||
|
|
||||||
|
PYTHON_VERSION = "3.13.15"
|
||||||
|
PYTHON_EMBED_URL = (
|
||||||
|
f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip"
|
||||||
|
)
|
||||||
|
GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
|
||||||
|
FFMPEG_URL = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1/ffmpeg-win32-x64.gz"
|
||||||
|
WEBVIEW_INSTALLER = "MicrosoftEdgeWebView2RuntimeInstallerX64.exe"
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
print(message, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: list[str], cwd: Path) -> None:
|
||||||
|
log("+ " + " ".join(command))
|
||||||
|
subprocess.run(command, cwd=cwd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def download(url: str, dest: Path) -> None:
|
||||||
|
if dest.is_file() and dest.stat().st_size > 0:
|
||||||
|
log(f"已有缓存 {dest.name}")
|
||||||
|
return
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = dest.with_suffix(dest.suffix + ".part")
|
||||||
|
log(f"下载 {url}")
|
||||||
|
request = urllib.request.Request(url, headers={"User-Agent": "RaptDrama"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response, temporary.open("wb") as handle:
|
||||||
|
total = int(response.headers.get("Content-Length") or 0)
|
||||||
|
done = 0
|
||||||
|
while True:
|
||||||
|
chunk = response.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
handle.write(chunk)
|
||||||
|
done += len(chunk)
|
||||||
|
if total:
|
||||||
|
log(f" {dest.name} {done * 100 // total}%")
|
||||||
|
except Exception:
|
||||||
|
if temporary.exists():
|
||||||
|
temporary.unlink()
|
||||||
|
raise
|
||||||
|
temporary.replace(dest)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tree(source: Path, dest: Path, ignore: shutil.IgnorePattern) -> None:
|
||||||
|
if dest.exists():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
shutil.copytree(source, dest, ignore=ignore)
|
||||||
|
|
||||||
|
|
||||||
|
def build_ui() -> None:
|
||||||
|
npm = "npm.cmd" if sys.platform == "win32" else "npm"
|
||||||
|
run([npm, "run", "build"], ROOT / "ui")
|
||||||
|
if not (DIST / "ui" / "index.html").is_file():
|
||||||
|
raise SystemExit("界面没有编译到 dist/ui")
|
||||||
|
|
||||||
|
|
||||||
|
def build_desktop() -> Path:
|
||||||
|
run(["cargo", "build", "--release"], ROOT / "src-tauri")
|
||||||
|
meta = subprocess.check_output(
|
||||||
|
["cargo", "metadata", "--format-version", "1", "--no-deps"],
|
||||||
|
cwd=ROOT / "src-tauri",
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
target = Path(json.loads(meta)["target_directory"]) / "release"
|
||||||
|
name = "raptdrama.exe" if sys.platform == "win32" else "raptdrama"
|
||||||
|
exe = target / name
|
||||||
|
if not exe.is_file():
|
||||||
|
raise SystemExit(f"没有找到桌面程序 {exe}")
|
||||||
|
return exe
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_python(stage: Path) -> None:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
raise SystemExit("当前只内置了 Windows 版 Python,其他平台的运行时还没接上")
|
||||||
|
archive = CACHE / f"python-{PYTHON_VERSION}-embed-amd64.zip"
|
||||||
|
download(PYTHON_EMBED_URL, archive)
|
||||||
|
target = stage / "runtime" / "python"
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(archive) as packed:
|
||||||
|
packed.extractall(target)
|
||||||
|
pth = next(target.glob("python*._pth"))
|
||||||
|
lines = []
|
||||||
|
saw_site = False
|
||||||
|
for line in pth.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.strip() == "#import site":
|
||||||
|
lines.append("import site")
|
||||||
|
saw_site = True
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
if line.strip() == "import site":
|
||||||
|
saw_site = True
|
||||||
|
if not any(item.strip().replace("\\", "/") == "Lib/site-packages" for item in lines):
|
||||||
|
lines.append("Lib/site-packages")
|
||||||
|
if not saw_site:
|
||||||
|
lines.append("import site")
|
||||||
|
pth.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
(target / "Lib" / "site-packages").mkdir(parents=True, exist_ok=True)
|
||||||
|
get_pip = CACHE / "get-pip.py"
|
||||||
|
download(GET_PIP_URL, get_pip)
|
||||||
|
python = target / "python.exe"
|
||||||
|
run([str(python), str(get_pip)], target)
|
||||||
|
run([str(python), "-m", "pip", "install", "-r", str(ROOT / "server" / "requirements.txt")], target)
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_adb(stage: Path) -> None:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
raise SystemExit("当前只内置了 Windows 版 adb,其他平台的文件还没放进 vendor")
|
||||||
|
source = ROOT / "vendor" / "platform-tools"
|
||||||
|
dest = stage / "tools" / "platform-tools"
|
||||||
|
if dest.exists():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
required = ["adb.exe", "AdbWinApi.dll", "AdbWinUsbApi.dll"]
|
||||||
|
missing = [name for name in required if not (source / name).is_file()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("vendor/platform-tools 里没有 " + "、".join(missing))
|
||||||
|
shutil.copytree(source, dest)
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_ffmpeg(stage: Path) -> None:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
raise SystemExit("当前只内置了 Windows 版 ffmpeg,其他平台的下载地址还没接上")
|
||||||
|
dest_dir = stage / "tools" / "ffmpeg"
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest = dest_dir / "ffmpeg.exe"
|
||||||
|
existing = ROOT / "tools" / "ffmpeg" / "ffmpeg.exe"
|
||||||
|
if existing.is_file():
|
||||||
|
shutil.copy2(existing, dest)
|
||||||
|
return
|
||||||
|
archive = CACHE / "ffmpeg-win32-x64.gz"
|
||||||
|
download(FFMPEG_URL, archive)
|
||||||
|
with gzip.open(archive, "rb") as packed:
|
||||||
|
dest.write_bytes(packed.read())
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_webview(stage: Path) -> None:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return
|
||||||
|
source = ROOT / "vendor" / "webview2" / WEBVIEW_INSTALLER
|
||||||
|
if not source.is_file():
|
||||||
|
raise SystemExit(f"vendor/webview2 里没有 {WEBVIEW_INSTALLER}")
|
||||||
|
dest = stage / "vendor" / "webview2"
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(source, dest / source.name)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_server(stage: Path) -> None:
|
||||||
|
copy_tree(
|
||||||
|
ROOT / "server",
|
||||||
|
stage / "server",
|
||||||
|
shutil.ignore_patterns(".venv", "__pycache__", "*.pyc", "*.pyo"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_zip(stage: Path) -> Path:
|
||||||
|
suffix = "windows-x64" if sys.platform == "win32" else platform.system().lower()
|
||||||
|
archive = DIST / f"{STAGE_NAME}-{suffix}.zip"
|
||||||
|
temporary = archive.with_name(archive.stem + ".writing.zip")
|
||||||
|
if temporary.exists():
|
||||||
|
temporary.unlink()
|
||||||
|
log(f"压缩 {archive}")
|
||||||
|
with zipfile.ZipFile(temporary, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as packed:
|
||||||
|
for path in stage.rglob("*"):
|
||||||
|
if not path.is_file() or path.name.lower() == "readme.md":
|
||||||
|
continue
|
||||||
|
packed.write(path, Path(STAGE_NAME) / path.relative_to(stage))
|
||||||
|
try:
|
||||||
|
if archive.exists():
|
||||||
|
archive.unlink()
|
||||||
|
temporary.replace(archive)
|
||||||
|
except PermissionError:
|
||||||
|
held = archive.with_name(archive.stem + "-new.zip")
|
||||||
|
if held.exists():
|
||||||
|
held.unlink()
|
||||||
|
temporary.replace(held)
|
||||||
|
raise SystemExit(f"{archive.name} 正在被占用,新的压缩包写在 {held}")
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
raise SystemExit("目前只完成了 Windows 打包。脚本按平台拆开了,macOS 和 Linux 可以以后补运行时。")
|
||||||
|
CACHE.mkdir(parents=True, exist_ok=True)
|
||||||
|
build_ui()
|
||||||
|
exe = build_desktop()
|
||||||
|
stage = DIST / STAGE_NAME
|
||||||
|
if stage.exists():
|
||||||
|
shutil.rmtree(stage)
|
||||||
|
stage.mkdir(parents=True)
|
||||||
|
shutil.copy2(exe, stage / exe.name)
|
||||||
|
copy_server(stage)
|
||||||
|
ui_dest = stage / "dist" / "ui"
|
||||||
|
shutil.copytree(DIST / "ui", ui_dest)
|
||||||
|
bundle_python(stage)
|
||||||
|
bundle_adb(stage)
|
||||||
|
bundle_ffmpeg(stage)
|
||||||
|
bundle_webview(stage)
|
||||||
|
archive = make_zip(stage)
|
||||||
|
log(f"完成 {archive} ({archive.stat().st_size / 1024 / 1024:.1f} MB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -24,10 +24,14 @@ class Adb:
|
|||||||
raise RuntimeError(f"找不到 adb:{explicit}")
|
raise RuntimeError(f"找不到 adb:{explicit}")
|
||||||
return cls(explicit)
|
return cls(explicit)
|
||||||
name = "adb.exe" if os.name == "nt" else "adb"
|
name = "adb.exe" if os.name == "nt" else "adb"
|
||||||
path = REPO_ROOT / "tools" / "platform-tools" / name
|
candidates = [
|
||||||
if not path.is_file():
|
REPO_ROOT / "tools" / "platform-tools" / name,
|
||||||
raise RuntimeError(f"项目内没有 adb:{path}")
|
REPO_ROOT / "vendor" / "platform-tools" / name,
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if path.is_file():
|
||||||
return cls(str(path))
|
return cls(str(path))
|
||||||
|
raise RuntimeError(f"项目内没有 adb:{candidates[0]}")
|
||||||
|
|
||||||
def list_devices(self) -> list[dict]:
|
def list_devices(self) -> list[dict]:
|
||||||
proc = self._run(["devices", "-l"])
|
proc = self._run(["devices", "-l"])
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from server.services.sessions import sessions
|
|||||||
CLASSIFY_MAX_PAGES = 50
|
CLASSIFY_MAX_PAGES = 50
|
||||||
RECOMMEND_MAX_PAGES = 80
|
RECOMMEND_MAX_PAGES = 80
|
||||||
FIXED_SECTIONS = (
|
FIXED_SECTIONS = (
|
||||||
|
{"key": "trending", "name": "Trending Now", "note": "", "loading": True},
|
||||||
{"key": "new_releases", "name": "New Releases", "note": "", "loading": True},
|
{"key": "new_releases", "name": "New Releases", "note": "", "loading": True},
|
||||||
{"key": "recommend", "name": "Recommend", "note": "", "loading": True},
|
{"key": "recommend", "name": "Recommend", "note": "", "loading": True},
|
||||||
)
|
)
|
||||||
@@ -25,7 +26,7 @@ class CatalogService:
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
def _categories(self, token: str, device: DeviceProfile) -> list[dict]:
|
def _categories(self, token: str, device: DeviceProfile) -> list[dict]:
|
||||||
data = http.request_data("POST", API_BASE + "/app/open/classifyv2", token, device, {})
|
data = http.request_data("GET", API_BASE + "/app/open/classifyv2", token, device, None)
|
||||||
rows = []
|
rows = []
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
for item in data:
|
for item in data:
|
||||||
@@ -72,14 +73,15 @@ class CatalogService:
|
|||||||
f"type-{item['id']}",
|
f"type-{item['id']}",
|
||||||
item["label"],
|
item["label"],
|
||||||
"/app/open/classify_video",
|
"/app/open/classify_video",
|
||||||
"POST",
|
"GET",
|
||||||
lambda page, type_id=item["id"]: {"page": page, "limit": 10, "type": type_id},
|
lambda page, type_id=item["id"]: {"type": type_id, "page": page},
|
||||||
CLASSIFY_MAX_PAGES,
|
CLASSIFY_MAX_PAGES,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
for item in categories
|
for item in categories
|
||||||
]
|
]
|
||||||
jobs.append(("new_releases", "New Releases", "/app/open/orgin", "GET", None, 1, ""))
|
jobs.append(("trending", "Trending Now", "/app/open/orgin?class=trending_now", "GET", None, 1, ""))
|
||||||
|
jobs.append(("new_releases", "New Releases", "/app/open/orgin?class=new_releases", "GET", None, 1, ""))
|
||||||
jobs.append((
|
jobs.append((
|
||||||
"recommend",
|
"recommend",
|
||||||
"Recommend",
|
"Recommend",
|
||||||
@@ -148,7 +150,9 @@ class CatalogService:
|
|||||||
page = 1
|
page = 1
|
||||||
sent = False
|
sent = False
|
||||||
while page <= max_pages:
|
while page <= max_pages:
|
||||||
data = http.request_data(method, API_BASE + path, token, device, body(page) if body else None)
|
query = body(page) if body and method == "GET" else None
|
||||||
|
payload = body(page) if body and method != "GET" else None
|
||||||
|
data = http.request_data(method, API_BASE + path, token, device, payload, query)
|
||||||
batch = data if isinstance(data, list) else []
|
batch = data if isinstance(data, list) else []
|
||||||
if not batch and isinstance(data, dict) and isinstance(data.get("list"), list):
|
if not batch and isinstance(data, dict) and isinstance(data.get("list"), list):
|
||||||
batch = data["list"]
|
batch = data["list"]
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ class DeviceProfile:
|
|||||||
"x-os-version": str(self.os_version),
|
"x-os-version": str(self.os_version),
|
||||||
"x-device-physical": physical,
|
"x-device-physical": physical,
|
||||||
"x-device-model": self.model,
|
"x-device-model": self.model,
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
"accept-language": self.language,
|
"accept-language": self.language,
|
||||||
"accept-language-custom": self.language,
|
"accept-language-custom": self.language,
|
||||||
|
"http_accept_language_custom": self.language.lower(),
|
||||||
"x-os-name": "Android",
|
"x-os-name": "Android",
|
||||||
"x-device-brand": self.brand,
|
"x-device-brand": self.brand,
|
||||||
"x-device-id": self.device_id,
|
"x-device-id": self.device_id,
|
||||||
|
|||||||
+11
-4
@@ -53,11 +53,11 @@ class HttpClient:
|
|||||||
raise RuntimeError("接口没有返回数据")
|
raise RuntimeError("接口没有返回数据")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def request_data(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None):
|
def request_data(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None, query: dict | None = None):
|
||||||
headers = device.headers()
|
headers = device.headers()
|
||||||
if token:
|
if token:
|
||||||
headers["apasstk"] = token
|
headers["apasstk"] = token
|
||||||
response = self._request(method, url, headers=headers, json=body)
|
response = self._request(method, url, headers, body or None, query)
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
if payload.get("code") != 200:
|
if payload.get("code") != 200:
|
||||||
raise RuntimeError(f"{method} {url} -> {payload.get('code')} {payload.get('msg')}")
|
raise RuntimeError(f"{method} {url} -> {payload.get('code')} {payload.get('msg')}")
|
||||||
@@ -65,11 +65,18 @@ class HttpClient:
|
|||||||
raise RuntimeError("接口没有返回数据")
|
raise RuntimeError("接口没有返回数据")
|
||||||
return payload.get("data")
|
return payload.get("data")
|
||||||
|
|
||||||
def _request(self, method: str, url: str, headers: dict, json: dict | None) -> httpx.Response:
|
def _request(self, method: str, url: str, headers: dict, body: dict | None, query: dict | None) -> httpx.Response:
|
||||||
last_error = None
|
last_error = None
|
||||||
|
options: dict = {"headers": headers}
|
||||||
|
if query:
|
||||||
|
options["params"] = query
|
||||||
|
if body:
|
||||||
|
options["json"] = body
|
||||||
|
elif method == "POST":
|
||||||
|
options["content"] = b""
|
||||||
for attempt in range(6):
|
for attempt in range(6):
|
||||||
try:
|
try:
|
||||||
return self._client_locked().request(method, url, headers=headers, json=json)
|
return self._client_locked().request(method, url, **options)
|
||||||
except (httpx.HTTPError, TimeoutError, OSError) as exc:
|
except (httpx.HTTPError, TimeoutError, OSError) as exc:
|
||||||
last_error = exc
|
last_error = exc
|
||||||
time.sleep(min(10, 1.5 * (attempt + 1)))
|
time.sleep(min(10, 1.5 * (attempt + 1)))
|
||||||
|
|||||||
+60
-7
@@ -18,13 +18,64 @@ fn repo_root() -> PathBuf {
|
|||||||
.expect("项目根目录")
|
.expect("项目根目录")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn python_command(root: &Path) -> PathBuf {
|
fn app_root() -> PathBuf {
|
||||||
let bundled = root.join("server").join(".venv").join("Scripts").join("python.exe");
|
if cfg!(debug_assertions) {
|
||||||
if bundled.is_file() {
|
return repo_root();
|
||||||
bundled
|
|
||||||
} else {
|
|
||||||
PathBuf::from("python")
|
|
||||||
}
|
}
|
||||||
|
std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| path.parent().map(Path::to_path_buf))
|
||||||
|
.filter(|path| path.join("server").is_dir())
|
||||||
|
.unwrap_or_else(repo_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn python_command(root: &Path) -> PathBuf {
|
||||||
|
let candidates = [
|
||||||
|
root.join("runtime").join("python").join("python.exe"),
|
||||||
|
root.join("runtime").join("python").join("bin").join("python3"),
|
||||||
|
root.join("server").join(".venv").join("Scripts").join("python.exe"),
|
||||||
|
root.join("server").join(".venv").join("bin").join("python3"),
|
||||||
|
];
|
||||||
|
for candidate in candidates {
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from(if cfg!(windows) { "python" } else { "python3" })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_webview_runtime(root: &Path) {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if webview_runtime_installed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let installer = root
|
||||||
|
.join("vendor")
|
||||||
|
.join("webview2")
|
||||||
|
.join("MicrosoftEdgeWebView2RuntimeInstallerX64.exe");
|
||||||
|
if installer.is_file() {
|
||||||
|
let _ = Command::new(installer).args(["/silent", "/install"]).status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn webview_runtime_installed() -> bool {
|
||||||
|
let keys = [
|
||||||
|
r"HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
|
||||||
|
r"HKLM\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
|
||||||
|
r"HKCU\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
|
||||||
|
];
|
||||||
|
keys.iter().any(|key| {
|
||||||
|
Command::new("reg")
|
||||||
|
.args(["query", key, "/v", "pv"])
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.status()
|
||||||
|
.map(|status| status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn free_port() -> Result<u16, String> {
|
fn free_port() -> Result<u16, String> {
|
||||||
@@ -148,7 +199,7 @@ fn disable_browser_chrome(window: &tauri::WebviewWindow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn boot(handle: &AppHandle, window: &tauri::WebviewWindow) -> Result<(), String> {
|
fn boot(handle: &AppHandle, window: &tauri::WebviewWindow) -> Result<(), String> {
|
||||||
let root = repo_root();
|
let root = app_root();
|
||||||
let mut last_error = "后台启动超时".to_string();
|
let mut last_error = "后台启动超时".to_string();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let port = free_port()?;
|
let port = free_port()?;
|
||||||
@@ -179,6 +230,8 @@ fn boot(handle: &AppHandle, window: &tauri::WebviewWindow) -> Result<(), String>
|
|||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
let root = app_root();
|
||||||
|
prepare_webview_runtime(&root);
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|||||||
Reference in New Issue
Block a user