Add automatic aria2 binary installation support
- New core/aria2_installer.py module downloads the latest aria2c release from GitHub for Windows, macOS, and Linux with safe path extraction. - New POST /model_resolver/aria2/install endpoint triggers installation and saves the resolved aria2c_path plus download_backend=aria2. - Options UI adds an Install aria2 button, hides aria2-specific fields when another backend is selected, and updates status on completion. - Includes .gitignore entry for tools/aria2/ so installed binaries are not committed.
This commit is contained in:
@@ -30,6 +30,9 @@ Thumbs.db
|
||||
*.cache
|
||||
scratch/
|
||||
|
||||
# Downloaded external tools
|
||||
tools/aria2/
|
||||
|
||||
# User settings – contains API keys and tokens, must NOT be committed
|
||||
model_resolver_settings.json
|
||||
tests/
|
||||
|
||||
+27
@@ -139,6 +139,7 @@ class ModelResolverExtension:
|
||||
normalize_download_category,
|
||||
write_lora_manager_metadata,
|
||||
)
|
||||
from .core.aria2_installer import Aria2InstallError, install_aria2_engine
|
||||
from .core.sources.popular import (
|
||||
get_popular_model_url,
|
||||
search_popular_models,
|
||||
@@ -3539,6 +3540,32 @@ class ModelResolverExtension:
|
||||
settings["aria2c_path"] = payload.get("aria2c_path", "")
|
||||
return web.json_response(await asyncio.to_thread(get_aria2_status, settings))
|
||||
|
||||
@routes.post("/model_resolver/aria2/install")
|
||||
@json_api_endpoint("aria2 install")
|
||||
async def aria2_install_route(request):
|
||||
"""Download official aria2c binary and save its path."""
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
payload = {}
|
||||
force = False
|
||||
if isinstance(payload, dict):
|
||||
force = to_bool(payload.get("force"), False)
|
||||
try:
|
||||
install_result = await asyncio.to_thread(install_aria2_engine, force)
|
||||
except Aria2InstallError as exc:
|
||||
self.logger.warning(f"Model Resolver aria2 install failed: {exc}")
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
settings = await asyncio.to_thread(
|
||||
save_resolver_settings,
|
||||
{
|
||||
"aria2c_path": install_result.get("aria2c_path", ""),
|
||||
"download_backend": "aria2",
|
||||
},
|
||||
)
|
||||
install_result["settings"] = settings
|
||||
return web.json_response(install_result)
|
||||
|
||||
@routes.get("/model_resolver/directories")
|
||||
@json_api_endpoint("directories")
|
||||
async def get_directories(request):
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
"""Install aria2c from official aria2 GitHub releases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .path_utils import is_path_within
|
||||
|
||||
|
||||
ARIA2_RELEASE_API_URL = "https://api.github.com/repos/aria2/aria2/releases/latest"
|
||||
REQUEST_HEADERS = {"User-Agent": "ComfyUI-Model-Resolver"}
|
||||
CUSTOM_NODE_ROOT = Path(__file__).resolve().parents[1]
|
||||
ARIA2_INSTALL_ROOT = CUSTOM_NODE_ROOT / "tools" / "aria2"
|
||||
|
||||
|
||||
class Aria2InstallError(RuntimeError):
|
||||
"""Raised when aria2 cannot be installed automatically."""
|
||||
|
||||
|
||||
def _safe_name(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
safe = "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)
|
||||
return safe.strip("._") or "aria2"
|
||||
|
||||
|
||||
def _machine_bits(machine: str) -> str:
|
||||
machine = str(machine or "").lower()
|
||||
if machine in {"amd64", "x86_64", "x64", "arm64", "aarch64"}:
|
||||
return "64"
|
||||
if machine in {"x86", "i386", "i686"}:
|
||||
return "32"
|
||||
return "64"
|
||||
|
||||
|
||||
def _platform_tokens() -> Dict[str, str]:
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "windows":
|
||||
bits = _machine_bits(machine)
|
||||
return {
|
||||
"system": "windows",
|
||||
"machine": machine,
|
||||
"label": f"Windows {bits}-bit",
|
||||
"required": "win",
|
||||
"arch": f"{bits}bit",
|
||||
"exe_name": "aria2c.exe",
|
||||
}
|
||||
|
||||
if system == "darwin":
|
||||
return {
|
||||
"system": "darwin",
|
||||
"machine": machine,
|
||||
"label": "macOS",
|
||||
"required": "mac",
|
||||
"arch": "",
|
||||
"exe_name": "aria2c",
|
||||
}
|
||||
|
||||
if system == "linux":
|
||||
return {
|
||||
"system": "linux",
|
||||
"machine": machine,
|
||||
"label": f"Linux {machine or 'unknown'}",
|
||||
"required": "linux",
|
||||
"arch": "",
|
||||
"exe_name": "aria2c",
|
||||
}
|
||||
|
||||
raise Aria2InstallError(
|
||||
f"Automatic aria2 install is not supported on {platform.system() or 'this OS'}."
|
||||
)
|
||||
|
||||
|
||||
def _asset_name(asset: Dict[str, Any]) -> str:
|
||||
return str(asset.get("name") or "").strip()
|
||||
|
||||
|
||||
def _asset_url(asset: Dict[str, Any]) -> str:
|
||||
return str(asset.get("browser_download_url") or "").strip()
|
||||
|
||||
|
||||
def _is_source_archive(name: str) -> bool:
|
||||
lower = name.lower()
|
||||
return lower.endswith((".tar.gz", ".tar.bz2", ".tar.xz")) and "build" not in lower
|
||||
|
||||
|
||||
def _score_asset(asset: Dict[str, Any], tokens: Dict[str, str]) -> int:
|
||||
name = _asset_name(asset)
|
||||
lower = name.lower()
|
||||
if not name or not _asset_url(asset):
|
||||
return -1
|
||||
if _is_source_archive(name):
|
||||
return -1
|
||||
if tokens["system"] != "linux" and "android" in lower:
|
||||
return -1
|
||||
|
||||
score = 0
|
||||
arch = tokens.get("arch") or ""
|
||||
|
||||
if tokens["system"] == "windows":
|
||||
if "win" not in lower:
|
||||
return -1
|
||||
score += 50
|
||||
if arch and arch in lower:
|
||||
score += 30
|
||||
if lower.endswith(".zip"):
|
||||
score += 10
|
||||
return score
|
||||
|
||||
if tokens["system"] == "darwin":
|
||||
if not any(token in lower for token in ("mac", "darwin", "osx", "os-x")):
|
||||
return -1
|
||||
score += 50
|
||||
if lower.endswith((".zip", ".tar.gz", ".tar.xz", ".tar.bz2")):
|
||||
score += 10
|
||||
return score
|
||||
|
||||
if tokens["system"] == "linux":
|
||||
if "linux" not in lower or "android" in lower:
|
||||
return -1
|
||||
score += 50
|
||||
machine = tokens.get("machine") or ""
|
||||
if machine and machine in lower:
|
||||
score += 10
|
||||
if lower.endswith((".zip", ".tar.gz", ".tar.xz", ".tar.bz2")):
|
||||
score += 10
|
||||
return score
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def _select_release_asset(release: Dict[str, Any], tokens: Dict[str, str]) -> Dict[str, Any]:
|
||||
assets = release.get("assets") or []
|
||||
if not isinstance(assets, list) or not assets:
|
||||
raise Aria2InstallError("The latest aria2 release does not expose downloadable assets.")
|
||||
|
||||
scored = [
|
||||
(score, asset)
|
||||
for asset in assets
|
||||
for score in [_score_asset(asset, tokens)]
|
||||
if score >= 0
|
||||
]
|
||||
if not scored:
|
||||
release_name = release.get("tag_name") or release.get("name") or "latest release"
|
||||
asset_names = ", ".join(_asset_name(asset) for asset in assets if _asset_name(asset))
|
||||
raise Aria2InstallError(
|
||||
f"No prebuilt aria2 desktop binary for {tokens['label']} was found in {release_name}. "
|
||||
"Install aria2 with your system package manager or configure aria2c_path manually. "
|
||||
f"Available assets: {asset_names or 'none'}"
|
||||
)
|
||||
|
||||
scored.sort(key=lambda item: item[0], reverse=True)
|
||||
return scored[0][1]
|
||||
|
||||
|
||||
def _fetch_latest_release() -> Dict[str, Any]:
|
||||
try:
|
||||
response = requests.get(ARIA2_RELEASE_API_URL, headers=REQUEST_HEADERS, timeout=(10, 30))
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as exc:
|
||||
raise Aria2InstallError(
|
||||
"Could not contact GitHub to check the latest aria2 release. "
|
||||
"Check your internet connection, firewall/proxy settings, or configure aria2c_path manually. "
|
||||
f"Details: {exc}"
|
||||
) from exc
|
||||
data = response.json()
|
||||
if not isinstance(data, dict):
|
||||
raise Aria2InstallError("GitHub returned an invalid aria2 release response.")
|
||||
return data
|
||||
|
||||
|
||||
def _assert_within_install_root(path: Path) -> None:
|
||||
install_root = ARIA2_INSTALL_ROOT.resolve()
|
||||
target = path.resolve()
|
||||
if target == install_root:
|
||||
return
|
||||
if not is_path_within(str(target), str(install_root)):
|
||||
raise Aria2InstallError(f"Refusing to write outside aria2 install root: {target}")
|
||||
|
||||
|
||||
def _safe_extract_zip(archive_path: Path, destination: Path) -> None:
|
||||
destination_abs = destination.resolve()
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
for member in archive.infolist():
|
||||
target = destination_abs / member.filename
|
||||
target_abs = target.resolve()
|
||||
if not is_path_within(str(target_abs), str(destination_abs)) and target_abs != destination_abs:
|
||||
raise Aria2InstallError(f"Unsafe path in aria2 archive: {member.filename}")
|
||||
archive.extractall(destination_abs)
|
||||
|
||||
|
||||
def _safe_extract_tar(archive_path: Path, destination: Path) -> None:
|
||||
destination_abs = destination.resolve()
|
||||
with tarfile.open(archive_path) as archive:
|
||||
for member in archive.getmembers():
|
||||
target = destination_abs / member.name
|
||||
target_abs = target.resolve()
|
||||
if not is_path_within(str(target_abs), str(destination_abs)) and target_abs != destination_abs:
|
||||
raise Aria2InstallError(f"Unsafe path in aria2 archive: {member.name}")
|
||||
archive.extractall(destination_abs)
|
||||
|
||||
|
||||
def _extract_archive(archive_path: Path, destination: Path) -> None:
|
||||
name = archive_path.name.lower()
|
||||
if name.endswith(".zip"):
|
||||
_safe_extract_zip(archive_path, destination)
|
||||
return
|
||||
if name.endswith((".tar.gz", ".tgz", ".tar.xz", ".tar.bz2")):
|
||||
_safe_extract_tar(archive_path, destination)
|
||||
return
|
||||
raise Aria2InstallError(f"Unsupported aria2 archive format: {archive_path.name}")
|
||||
|
||||
|
||||
def _find_executable(root: Path, exe_name: str) -> Optional[Path]:
|
||||
if not root.exists():
|
||||
return None
|
||||
expected = exe_name.lower()
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file() and path.name.lower() == expected:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _find_existing_install(exe_name: str) -> Optional[Path]:
|
||||
candidates = []
|
||||
expected = exe_name.lower()
|
||||
if not ARIA2_INSTALL_ROOT.exists():
|
||||
return None
|
||||
for path in ARIA2_INSTALL_ROOT.rglob("*"):
|
||||
if path.is_file() and path.name.lower() == expected:
|
||||
candidates.append(path)
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda path: path.stat().st_mtime, reverse=True)
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def _existing_install_metadata(executable: Path) -> Dict[str, str]:
|
||||
try:
|
||||
relative = executable.relative_to(ARIA2_INSTALL_ROOT)
|
||||
parts = relative.parts
|
||||
except ValueError:
|
||||
parts = ()
|
||||
|
||||
return {
|
||||
"version": parts[0] if parts else "",
|
||||
"asset": parts[1] if len(parts) > 1 else "",
|
||||
"install_dir": str(executable.parent),
|
||||
}
|
||||
|
||||
|
||||
def _download_file(url: str, destination: Path) -> int:
|
||||
total = 0
|
||||
try:
|
||||
with requests.get(url, headers=REQUEST_HEADERS, stream=True, timeout=(10, 120)) as response:
|
||||
response.raise_for_status()
|
||||
with destination.open("wb") as handle:
|
||||
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
||||
if not chunk:
|
||||
continue
|
||||
handle.write(chunk)
|
||||
total += len(chunk)
|
||||
except requests.RequestException as exc:
|
||||
raise Aria2InstallError(
|
||||
"Could not download the aria2 release asset from GitHub. "
|
||||
"Check your internet connection, firewall/proxy settings, or configure aria2c_path manually. "
|
||||
f"Details: {exc}"
|
||||
) from exc
|
||||
return total
|
||||
|
||||
|
||||
def _chmod_executable(path: Path) -> None:
|
||||
if platform.system().lower() == "windows":
|
||||
return
|
||||
try:
|
||||
mode = path.stat().st_mode
|
||||
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def install_aria2_engine(force: bool = False) -> Dict[str, Any]:
|
||||
"""Download and install aria2c for the current platform."""
|
||||
tokens = _platform_tokens()
|
||||
if not force:
|
||||
existing_executable = _find_existing_install(tokens["exe_name"])
|
||||
if existing_executable and existing_executable.exists():
|
||||
_chmod_executable(existing_executable)
|
||||
existing_metadata = _existing_install_metadata(existing_executable)
|
||||
return {
|
||||
"success": True,
|
||||
"already_installed": True,
|
||||
"aria2c_path": str(existing_executable),
|
||||
"version": existing_metadata["version"],
|
||||
"asset": existing_metadata["asset"],
|
||||
"platform": tokens["label"],
|
||||
"install_dir": existing_metadata["install_dir"],
|
||||
}
|
||||
|
||||
release = _fetch_latest_release()
|
||||
asset = _select_release_asset(release, tokens)
|
||||
tag_name = str(release.get("tag_name") or "latest").strip() or "latest"
|
||||
asset_name = _asset_name(asset)
|
||||
asset_url = _asset_url(asset)
|
||||
if not asset_url:
|
||||
raise Aria2InstallError(f"aria2 release asset {asset_name or '<unknown>'} has no download URL.")
|
||||
|
||||
install_dir = ARIA2_INSTALL_ROOT / _safe_name(tag_name) / _safe_name(Path(asset_name).stem)
|
||||
_assert_within_install_root(install_dir)
|
||||
existing_executable = _find_executable(install_dir, tokens["exe_name"])
|
||||
if existing_executable and existing_executable.exists() and not force:
|
||||
_chmod_executable(existing_executable)
|
||||
return {
|
||||
"success": True,
|
||||
"already_installed": True,
|
||||
"aria2c_path": str(existing_executable),
|
||||
"version": tag_name,
|
||||
"asset": asset_name,
|
||||
"platform": tokens["label"],
|
||||
"install_dir": str(install_dir),
|
||||
}
|
||||
|
||||
ARIA2_INSTALL_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
install_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="model-resolver-aria2-") as tmp_dir_text:
|
||||
tmp_dir = Path(tmp_dir_text)
|
||||
archive_path = tmp_dir / asset_name
|
||||
downloaded_bytes = _download_file(asset_url, archive_path)
|
||||
|
||||
if install_dir.exists():
|
||||
_assert_within_install_root(install_dir)
|
||||
shutil.rmtree(install_dir)
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
_extract_archive(archive_path, install_dir)
|
||||
|
||||
executable = _find_executable(install_dir, tokens["exe_name"])
|
||||
if not executable:
|
||||
raise Aria2InstallError(
|
||||
f"Installed aria2 archive did not contain {tokens['exe_name']}."
|
||||
)
|
||||
_chmod_executable(executable)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"already_installed": False,
|
||||
"aria2c_path": str(executable),
|
||||
"version": tag_name,
|
||||
"asset": asset_name,
|
||||
"platform": tokens["label"],
|
||||
"install_dir": str(install_dir),
|
||||
"downloaded_bytes": downloaded_bytes,
|
||||
"download_url": asset_url,
|
||||
}
|
||||
@@ -6842,6 +6842,14 @@ a.mr-image-preview-resource-name:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mr-options-wide-row {
|
||||
grid-template-columns: minmax(150px, 220px) minmax(260px, 1fr);
|
||||
}
|
||||
|
||||
.mr-options-aria2-setting[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mr-options-path-mode-row {
|
||||
grid-template-columns: minmax(150px, 1fr) minmax(220px, 280px);
|
||||
}
|
||||
@@ -6923,6 +6931,10 @@ a.mr-image-preview-resource-name:hover {
|
||||
grid-template-columns: minmax(120px, 1fr) minmax(220px, 260px);
|
||||
}
|
||||
|
||||
.mr-options-wide-row {
|
||||
grid-template-columns: minmax(120px, 180px) minmax(220px, 1fr);
|
||||
}
|
||||
|
||||
.mr-options-toggle-row {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ export const optionsMethods = {
|
||||
<h5 class="mr-options-subsection-title">Download Engine</h5>
|
||||
<span class="mr-tooltip-badge" data-tooltip="Choose how model files are downloaded. aria2 requires the external aria2c executable and is intended for large downloads.">?</span>
|
||||
</div>
|
||||
<div class="mr-options-number-row">
|
||||
<div class="mr-options-number-row mr-options-wide-row">
|
||||
<div class="mr-options-number-copy">
|
||||
<span class="mr-options-label">Download backend</span>
|
||||
</div>
|
||||
@@ -243,22 +243,31 @@ export const optionsMethods = {
|
||||
${renderDownloadBackendOptions(tokens.download_backend)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mr-options-number-row">
|
||||
<div class="mr-options-number-copy">
|
||||
<span class="mr-options-label">aria2c path <span class="mr-tooltip-badge" data-tooltip="Optional full path to aria2c/aria2c.exe. Leave empty to use aria2c from your system PATH.">?</span></span>
|
||||
<div class="mr-options-dependent-block mr-options-aria2-setting">
|
||||
<div class="mr-options-number-row mr-options-wide-row">
|
||||
<div class="mr-options-number-copy">
|
||||
<span class="mr-options-label">aria2c path <span class="mr-tooltip-badge" data-tooltip="Optional full path to aria2c/aria2c.exe. Leave empty to use aria2c from your system PATH.">?</span></span>
|
||||
</div>
|
||||
<input id="mr-options-aria2c-path" class="mr-options-input" type="text" value="${this.escapeHtml(tokens.aria2c_path || '')}" placeholder="Leave empty to use aria2c from PATH">
|
||||
</div>
|
||||
<input id="mr-options-aria2c-path" class="mr-options-input" type="text" value="${this.escapeHtml(tokens.aria2c_path || '')}" placeholder="Leave empty to use aria2c from PATH">
|
||||
<div class="mr-options-db-actions">
|
||||
<button id="mr-options-aria2-install" type="button" class="mr-btn mr-btn-primary">${getSvgIcon('download')} Install aria2</button>
|
||||
<button id="mr-options-aria2-check" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('refreshCw')} Check aria2</button>
|
||||
<a class="mr-options-inline-link" href="https://github.com/Azornes/Comfyui-Model-Resolver/wiki/Aria2-Download-Backend" target="_blank" rel="noopener noreferrer">Setup guide</a>
|
||||
</div>
|
||||
<div id="mr-options-aria2-status" class="mr-options-db-message">aria2 requires aria2c installed or a configured executable path.</div>
|
||||
</div>
|
||||
<div class="mr-options-db-actions">
|
||||
<button id="mr-options-aria2-check" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('refreshCw')} Check aria2</button>
|
||||
<a class="mr-options-inline-link" href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Aria2-Download-Backend-(Experimental)" target="_blank" rel="noopener noreferrer">Setup guide</a>
|
||||
</div>
|
||||
<div id="mr-options-aria2-status" class="mr-options-db-message">Python downloader is used by default. aria2 requires aria2c installed or a configured executable path.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-options-panel">
|
||||
<div class="mr-options-toggle-list">
|
||||
${sourceDefaultsRows}
|
||||
<div class="mr-options-stack">
|
||||
<div class="mr-options-subsection-head">
|
||||
<h5 class="mr-options-subsection-title">Search Sources</h5>
|
||||
<span class="mr-tooltip-badge" data-tooltip="Choose which sources Model Resolver can use when searching for downloadable model matches.">?</span>
|
||||
</div>
|
||||
<div class="mr-options-toggle-list">
|
||||
${sourceDefaultsRows}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,8 +675,10 @@ export const optionsMethods = {
|
||||
const autoRefreshComfyModelsInput = this.contentElement.querySelector('#mr-options-auto-refresh-comfy-models');
|
||||
const downloadBackendInput = this.contentElement.querySelector('#mr-options-download-backend');
|
||||
const aria2cPathInput = this.contentElement.querySelector('#mr-options-aria2c-path');
|
||||
const aria2InstallBtn = this.contentElement.querySelector('#mr-options-aria2-install');
|
||||
const aria2CheckBtn = this.contentElement.querySelector('#mr-options-aria2-check');
|
||||
const aria2StatusEl = this.contentElement.querySelector('#mr-options-aria2-status');
|
||||
const aria2SettingEls = Array.from(this.contentElement.querySelectorAll('.mr-options-aria2-setting'));
|
||||
const downloadPathModeInput = this.contentElement.querySelector('#mr-options-download-path-mode');
|
||||
const defaultRootSelectInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-default-root'));
|
||||
const templatePresetInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-template-preset'));
|
||||
@@ -779,6 +790,14 @@ export const optionsMethods = {
|
||||
if (mode) aria2StatusEl.classList.add(mode);
|
||||
};
|
||||
|
||||
const syncAria2OptionsVisibility = () => {
|
||||
const backend = this.normalizeDownloadBackend(downloadBackendInput?.value || tokens.download_backend);
|
||||
const showAria2 = backend === 'aria2';
|
||||
aria2SettingEls.forEach((element) => {
|
||||
element.hidden = !showAria2;
|
||||
});
|
||||
};
|
||||
|
||||
const checkAria2Status = async ({ useUnsavedPath = true } = {}) => {
|
||||
if (aria2CheckBtn) aria2CheckBtn.disabled = true;
|
||||
setAria2Status('Checking aria2...', 'is-pending');
|
||||
@@ -806,6 +825,47 @@ export const optionsMethods = {
|
||||
}
|
||||
};
|
||||
|
||||
const installAria2 = async () => {
|
||||
if (aria2InstallBtn) aria2InstallBtn.disabled = true;
|
||||
if (aria2CheckBtn) aria2CheckBtn.disabled = true;
|
||||
setAria2Status('Downloading and installing aria2 from official GitHub releases...', 'is-pending');
|
||||
try {
|
||||
const data = await this.fetchJson('/model_resolver/aria2/install', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
silent: true
|
||||
}, 'Install aria2');
|
||||
|
||||
if (!data?.success || !data?.aria2c_path) {
|
||||
throw new Error(data?.error || 'aria2 install did not return an executable path.');
|
||||
}
|
||||
|
||||
if (aria2cPathInput) {
|
||||
aria2cPathInput.value = data.aria2c_path;
|
||||
aria2cPathInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
if (downloadBackendInput) {
|
||||
downloadBackendInput.value = 'aria2';
|
||||
downloadBackendInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
tokens.aria2c_path = data.aria2c_path;
|
||||
tokens.download_backend = 'aria2';
|
||||
localStorage.setItem('ModelResolver.aria2cPath', data.aria2c_path);
|
||||
localStorage.setItem('ModelResolver.downloadBackend', 'aria2');
|
||||
|
||||
const versionText = data.version ? ` ${data.version}` : '';
|
||||
const installedText = data.already_installed ? 'already installed' : 'installed';
|
||||
setAria2Status(`aria2${versionText} ${installedText}: ${data.aria2c_path}`, 'is-valid');
|
||||
await checkAria2Status({ useUnsavedPath: true });
|
||||
} catch (error) {
|
||||
setAria2Status(error.message || 'aria2 install failed.', 'is-invalid');
|
||||
} finally {
|
||||
if (aria2InstallBtn) aria2InstallBtn.disabled = false;
|
||||
if (aria2CheckBtn) aria2CheckBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCredential = async ({ input, button, statusEl, endpoint, payloadKey, missingText }) => {
|
||||
const value = (input?.value || '').trim();
|
||||
if (!value) {
|
||||
@@ -1585,7 +1645,25 @@ export const optionsMethods = {
|
||||
});
|
||||
}
|
||||
|
||||
if (tokens.download_backend === 'aria2') {
|
||||
if (aria2InstallBtn) {
|
||||
aria2InstallBtn.addEventListener('click', () => {
|
||||
installAria2();
|
||||
});
|
||||
}
|
||||
|
||||
if (downloadBackendInput) {
|
||||
downloadBackendInput.addEventListener('change', () => {
|
||||
const backend = this.normalizeDownloadBackend(downloadBackendInput.value);
|
||||
syncAria2OptionsVisibility();
|
||||
if (backend === 'aria2') {
|
||||
checkAria2Status({ useUnsavedPath: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncAria2OptionsVisibility();
|
||||
|
||||
if (this.normalizeDownloadBackend(tokens.download_backend) === 'aria2') {
|
||||
checkAria2Status({ useUnsavedPath: false });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user