update hydrus_web_gui.py
This commit is contained in:
+163
-5
@@ -116,6 +116,7 @@ def run(cmd: list[str], quiet: bool = False, debug: bool = False, cwd: Optional[
|
|||||||
|
|
||||||
REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git"
|
REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git"
|
||||||
HYDRUS_REPO_URL = "https://github.com/hydrusnetwork/hydrus.git"
|
HYDRUS_REPO_URL = "https://github.com/hydrusnetwork/hydrus.git"
|
||||||
|
HYDRUS_WEB_GUI_REPO_URL = "https://code.glowers.club/goyimnose/api-HydrusNetwork.git"
|
||||||
HYDRUS_INSTALLER_SCRIPT_URLS = (
|
HYDRUS_INSTALLER_SCRIPT_URLS = (
|
||||||
"https://code.glowers.club/goyimnose/Medios-Macina/raw/branch/main/scripts/hydrusnetwork.py",
|
"https://code.glowers.club/goyimnose/Medios-Macina/raw/branch/main/scripts/hydrusnetwork.py",
|
||||||
)
|
)
|
||||||
@@ -510,6 +511,39 @@ def main() -> int:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Verify that the 'mm' command was installed correctly",
|
help="Verify that the 'mm' command was installed correctly",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--install-hydrus-web-gui",
|
||||||
|
action="store_true",
|
||||||
|
help="Clone/update api-HydrusNetwork, install npm dependencies, and prepare the Hydrus web GUI",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hydrus-web-gui-root",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Root directory where the Hydrus web GUI should be installed (default: the Medios repo root)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hydrus-web-gui-dest-name",
|
||||||
|
type=str,
|
||||||
|
default="api-HydrusNetwork",
|
||||||
|
help="Folder name for the Hydrus web GUI checkout (default: api-HydrusNetwork)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--install-hydrus-web-gui-service",
|
||||||
|
action="store_true",
|
||||||
|
help="Register the Hydrus web GUI to start automatically using systemd on Linux or Task Scheduler on Windows",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--setup-hydrus-web-gui-mpv-handler",
|
||||||
|
action="store_true",
|
||||||
|
help="Run the Hydrus web GUI's mpv-handler setup helper after installation (Windows/Linux)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hydrus-web-gui-service-name",
|
||||||
|
type=str,
|
||||||
|
default="hydrus-web-gui",
|
||||||
|
help="Service name used for the Hydrus web GUI auto-start registration",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--uninstall",
|
"--uninstall",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -523,6 +557,9 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.install_hydrus_web_gui_service or args.setup_hydrus_web_gui_mpv_handler:
|
||||||
|
args.install_hydrus_web_gui = True
|
||||||
|
|
||||||
# Ensure repo_root is always the project root, not the current working directory
|
# Ensure repo_root is always the project root, not the current working directory
|
||||||
# This prevents issues when bootstrap.py is run from different directories
|
# This prevents issues when bootstrap.py is run from different directories
|
||||||
try:
|
try:
|
||||||
@@ -841,13 +878,19 @@ def main() -> int:
|
|||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# Define menu options
|
# Define menu options
|
||||||
|
service_option = (
|
||||||
|
"5) Install Hydrus System Service (Auto-update + Headless)"
|
||||||
|
if installed
|
||||||
|
else "4) Install Hydrus System Service (Auto-update + Headless)"
|
||||||
|
)
|
||||||
options = [
|
options = [
|
||||||
"1) Reinstall" if installed else "1) Install",
|
"1) Reinstall" if installed else "1) Install",
|
||||||
"2) Extras > HydrusNetwork"
|
"2) Extras > HydrusNetwork",
|
||||||
|
"3) Extras > Hydrus Web GUI",
|
||||||
]
|
]
|
||||||
if installed:
|
if installed:
|
||||||
options.append("3) Uninstall")
|
options.append("4) Uninstall")
|
||||||
options.append("4) Install Hydrus System Service (Auto-update + Headless)")
|
options.append(service_option)
|
||||||
options.append("q) Quit")
|
options.append("q) Quit")
|
||||||
|
|
||||||
# Center the block of options by finding the longest one
|
# Center the block of options by finding the longest one
|
||||||
@@ -870,10 +913,13 @@ def main() -> int:
|
|||||||
if choice in ("2", "extras", "hydrus"):
|
if choice in ("2", "extras", "hydrus"):
|
||||||
return "extras_hydrus"
|
return "extras_hydrus"
|
||||||
|
|
||||||
if installed and choice in ("3", "uninstall"):
|
if choice in ("3", "web", "webgui", "gui"):
|
||||||
|
return "extras_hydrus_web_gui"
|
||||||
|
|
||||||
|
if installed and choice in ("4", "uninstall"):
|
||||||
return "uninstall"
|
return "uninstall"
|
||||||
|
|
||||||
if choice == "4":
|
if (installed and choice == "5") or (not installed and choice == "4"):
|
||||||
return "install_service"
|
return "install_service"
|
||||||
|
|
||||||
if choice in ("q", "quit", "exit"):
|
if choice in ("q", "quit", "exit"):
|
||||||
@@ -904,6 +950,40 @@ def main() -> int:
|
|||||||
print("\nHydrus installation cancelled.")
|
print("\nHydrus installation cancelled.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _prompt_hydrus_web_gui_location() -> tuple[Path, str, bool, bool] | None:
|
||||||
|
"""Ask the user for the Hydrus web GUI installation settings."""
|
||||||
|
default_root = repo_root if repo_root else Path.home()
|
||||||
|
default_dest_name = "api-HydrusNetwork"
|
||||||
|
print("\n[Hydrus Web GUI Installation]")
|
||||||
|
print("Install the api-HydrusNetwork web UI and optionally register it to auto-start.")
|
||||||
|
try:
|
||||||
|
root_input = input(f"Root directory [{default_root}]: ").strip()
|
||||||
|
if root_input:
|
||||||
|
if len(root_input) == 2 and root_input[1] == ":" and root_input[0].isalpha():
|
||||||
|
root_input += "\\"
|
||||||
|
root_path = Path(os.path.expandvars(os.path.expanduser(root_input))).resolve()
|
||||||
|
else:
|
||||||
|
root_path = default_root
|
||||||
|
|
||||||
|
dest_input = input(f"Folder name [{default_dest_name}]: ").strip()
|
||||||
|
dest_name = dest_input or default_dest_name
|
||||||
|
|
||||||
|
service_default = "Y"
|
||||||
|
service_input = input(
|
||||||
|
"Install auto-start service with systemd/Task Scheduler? [Y/n]: "
|
||||||
|
).strip().lower()
|
||||||
|
install_service = service_input not in {"n", "no"}
|
||||||
|
|
||||||
|
setup_mpv_input = input(
|
||||||
|
"Run mpv-handler setup after install? [y/N]: "
|
||||||
|
).strip().lower()
|
||||||
|
setup_mpv_handler = setup_mpv_input in {"y", "yes"}
|
||||||
|
|
||||||
|
return root_path, dest_name, install_service, setup_mpv_handler
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nHydrus web GUI installation cancelled.")
|
||||||
|
return None
|
||||||
|
|
||||||
def _clone_repo(url: str, dest: Path, depth: int = 1) -> bool:
|
def _clone_repo(url: str, dest: Path, depth: int = 1) -> bool:
|
||||||
"""Helper to clone a repository."""
|
"""Helper to clone a repository."""
|
||||||
try:
|
try:
|
||||||
@@ -935,6 +1015,45 @@ def main() -> int:
|
|||||||
print("Error: Failed to download Hydrus installer script", file=sys.stderr)
|
print("Error: Failed to download Hydrus installer script", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _run_hydrus_web_gui_installer(
|
||||||
|
install_root: Path,
|
||||||
|
install_dest: str,
|
||||||
|
*,
|
||||||
|
install_service: bool = False,
|
||||||
|
setup_mpv_handler: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
web_gui_script = script_dir / "hydrus_web_gui.py"
|
||||||
|
if not web_gui_script.exists():
|
||||||
|
print(f"Error: {web_gui_script} not found.", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
str(web_gui_script),
|
||||||
|
"--root",
|
||||||
|
str(install_root),
|
||||||
|
"--dest-name",
|
||||||
|
install_dest,
|
||||||
|
"--repo",
|
||||||
|
HYDRUS_WEB_GUI_REPO_URL,
|
||||||
|
]
|
||||||
|
if install_service:
|
||||||
|
cmd.extend([
|
||||||
|
"--install-service",
|
||||||
|
"--service-name",
|
||||||
|
args.hydrus_web_gui_service_name,
|
||||||
|
])
|
||||||
|
if setup_mpv_handler:
|
||||||
|
cmd.append("--setup-mpv-handler")
|
||||||
|
if args.debug:
|
||||||
|
cmd.append("--debug")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, stdin=sys.stdin)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
def _ensure_repo_available() -> bool:
|
def _ensure_repo_available() -> bool:
|
||||||
"""Prompt for a clone location when running outside the repository."""
|
"""Prompt for a clone location when running outside the repository."""
|
||||||
nonlocal repo_root, script_dir, is_in_repo
|
nonlocal repo_root, script_dir, is_in_repo
|
||||||
@@ -1255,6 +1374,27 @@ def main() -> int:
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stdin.readline()
|
sys.stdin.readline()
|
||||||
continue
|
continue
|
||||||
|
elif sel == "extras_hydrus_web_gui":
|
||||||
|
if not _ensure_repo_available():
|
||||||
|
return 1
|
||||||
|
web_gui_location = _prompt_hydrus_web_gui_location()
|
||||||
|
if web_gui_location is None:
|
||||||
|
continue
|
||||||
|
install_root, install_dest, install_service, setup_mpv_handler = web_gui_location
|
||||||
|
ok = _run_hydrus_web_gui_installer(
|
||||||
|
install_root,
|
||||||
|
install_dest,
|
||||||
|
install_service=install_service,
|
||||||
|
setup_mpv_handler=setup_mpv_handler,
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
print("\nHydrus web GUI installation finished.")
|
||||||
|
else:
|
||||||
|
print("\nHydrus web GUI installation failed.")
|
||||||
|
sys.stdout.write("Press Enter to return to menu...")
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stdin.readline()
|
||||||
|
continue
|
||||||
elif sel == "install_service":
|
elif sel == "install_service":
|
||||||
# Direct path input for the target repository
|
# Direct path input for the target repository
|
||||||
print("\n[ SYSTEM SERVICE INSTALLATION ]")
|
print("\n[ SYSTEM SERVICE INSTALLATION ]")
|
||||||
@@ -1371,6 +1511,8 @@ def main() -> int:
|
|||||||
total_steps += 1
|
total_steps += 1
|
||||||
if not getattr(args, "no_deno", False):
|
if not getattr(args, "no_deno", False):
|
||||||
total_steps += 1
|
total_steps += 1
|
||||||
|
if args.install_hydrus_web_gui:
|
||||||
|
total_steps += 1
|
||||||
|
|
||||||
pb = ProgressBar(total_steps, quiet=args.quiet or args.debug)
|
pb = ProgressBar(total_steps, quiet=args.quiet or args.debug)
|
||||||
|
|
||||||
@@ -1590,6 +1732,22 @@ def main() -> int:
|
|||||||
if not _check_deno_installed():
|
if not _check_deno_installed():
|
||||||
_install_deno(args.deno_version)
|
_install_deno(args.deno_version)
|
||||||
|
|
||||||
|
hydrus_web_gui_root = (
|
||||||
|
Path(os.path.expandvars(os.path.expanduser(args.hydrus_web_gui_root))).resolve()
|
||||||
|
if args.hydrus_web_gui_root
|
||||||
|
else repo_root
|
||||||
|
)
|
||||||
|
if args.install_hydrus_web_gui:
|
||||||
|
pb.update("Installing Hydrus web GUI...")
|
||||||
|
ok = _run_hydrus_web_gui_installer(
|
||||||
|
hydrus_web_gui_root,
|
||||||
|
args.hydrus_web_gui_dest_name,
|
||||||
|
install_service=args.install_hydrus_web_gui_service,
|
||||||
|
setup_mpv_handler=args.setup_hydrus_web_gui_mpv_handler,
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
return 1
|
||||||
|
|
||||||
# 10. Finalizing setup
|
# 10. Finalizing setup
|
||||||
pb.update("Writing launcher scripts...")
|
pb.update("Writing launcher scripts...")
|
||||||
def _write_launchers() -> None:
|
def _write_launchers() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Install and manage the Hydrus web GUI companion app.
|
||||||
|
|
||||||
|
This helper keeps the Medios bootstrap as the single user-facing installer entry
|
||||||
|
point while isolating the Node/npm-specific logic required by
|
||||||
|
`api-HydrusNetwork`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_REPO_URL = "https://code.glowers.club/goyimnose/api-HydrusNetwork.git"
|
||||||
|
DEFAULT_DEST_NAME = "api-HydrusNetwork"
|
||||||
|
DEFAULT_SERVICE_NAME = "hydrus-web-gui"
|
||||||
|
DEFAULT_PORT = 4173
|
||||||
|
|
||||||
|
|
||||||
|
def run(
|
||||||
|
cmd: list[str],
|
||||||
|
*,
|
||||||
|
cwd: Path | None = None,
|
||||||
|
check: bool = True,
|
||||||
|
capture_output: bool = False,
|
||||||
|
debug: bool = False,
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
if debug:
|
||||||
|
location = f" (cwd={cwd})" if cwd else ""
|
||||||
|
print(f"> {' '.join(cmd)}{location}")
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=str(cwd) if cwd else None,
|
||||||
|
check=check,
|
||||||
|
text=capture_output,
|
||||||
|
capture_output=capture_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_executable(*names: str) -> str | None:
|
||||||
|
for name in names:
|
||||||
|
found = shutil.which(name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def detect_npm() -> str | None:
|
||||||
|
if os.name == "nt":
|
||||||
|
return find_executable("npm.cmd", "npm")
|
||||||
|
return find_executable("npm")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_node_major_version(node_exe: str, debug: bool = False) -> int | None:
|
||||||
|
try:
|
||||||
|
result = run([node_exe, "--version"], capture_output=True, debug=debug)
|
||||||
|
version_text = (result.stdout or "").strip()
|
||||||
|
if version_text.startswith("v"):
|
||||||
|
version_text = version_text[1:]
|
||||||
|
major = version_text.split(".", 1)[0]
|
||||||
|
return int(major)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def with_privilege(cmd: list[str]) -> list[str]:
|
||||||
|
if os.name == "nt":
|
||||||
|
return cmd
|
||||||
|
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||||
|
return cmd
|
||||||
|
sudo = shutil.which("sudo")
|
||||||
|
if sudo:
|
||||||
|
return [sudo, *cmd]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_node_runtime(debug: bool = False) -> tuple[str, str]:
|
||||||
|
node_exe = find_executable("node")
|
||||||
|
npm_exe = detect_npm()
|
||||||
|
major = parse_node_major_version(node_exe, debug=debug) if node_exe else None
|
||||||
|
|
||||||
|
if node_exe and npm_exe and major is not None and major >= 20:
|
||||||
|
return node_exe, npm_exe
|
||||||
|
|
||||||
|
system = platform.system().lower()
|
||||||
|
print("Node.js 20+ and npm are required for the Hydrus web GUI. Attempting installation...")
|
||||||
|
|
||||||
|
if system == "windows":
|
||||||
|
winget = find_executable("winget")
|
||||||
|
if not winget:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Node.js 20+ is required. Install it manually or ensure winget is available."
|
||||||
|
)
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
winget,
|
||||||
|
"install",
|
||||||
|
"--id",
|
||||||
|
"OpenJS.NodeJS.LTS",
|
||||||
|
"-e",
|
||||||
|
"--accept-package-agreements",
|
||||||
|
"--accept-source-agreements",
|
||||||
|
],
|
||||||
|
debug=debug,
|
||||||
|
)
|
||||||
|
elif system == "linux":
|
||||||
|
if find_executable("apt"):
|
||||||
|
run(with_privilege(["apt", "update"]), debug=debug)
|
||||||
|
run(with_privilege(["apt", "install", "-y", "nodejs", "npm"]), debug=debug)
|
||||||
|
elif find_executable("dnf"):
|
||||||
|
run(with_privilege(["dnf", "install", "-y", "nodejs", "npm"]), debug=debug)
|
||||||
|
elif find_executable("pacman"):
|
||||||
|
run(with_privilege(["pacman", "-Sy", "--noconfirm", "nodejs", "npm"]), debug=debug)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Node.js 20+ is required. Install nodejs/npm manually for this distribution."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Automatic Hydrus web GUI setup is currently supported on Windows and Linux.")
|
||||||
|
|
||||||
|
node_exe = find_executable("node")
|
||||||
|
npm_exe = detect_npm()
|
||||||
|
major = parse_node_major_version(node_exe, debug=debug) if node_exe else None
|
||||||
|
if not node_exe or not npm_exe or major is None or major < 20:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Node.js installation completed, but Node.js 20+ with npm is still not available on PATH."
|
||||||
|
)
|
||||||
|
return node_exe, npm_exe
|
||||||
|
|
||||||
|
|
||||||
|
def clone_or_update_repo(repo_url: str, dest: Path, force: bool = False, debug: bool = False) -> None:
|
||||||
|
git = find_executable("git")
|
||||||
|
if not git:
|
||||||
|
raise RuntimeError("git is required to install the Hydrus web GUI.")
|
||||||
|
|
||||||
|
if dest.exists() and force:
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
if (dest / ".git").exists():
|
||||||
|
run([git, "-C", str(dest), "pull", "--ff-only"], debug=debug)
|
||||||
|
return
|
||||||
|
if any(dest.iterdir()):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Destination already exists and is not a git repository: {dest}. Use --force to replace it."
|
||||||
|
)
|
||||||
|
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
run([git, "clone", "--depth", "1", repo_url, str(dest)], debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
def install_web_gui_dependencies(app_root: Path, npm_exe: str, debug: bool = False) -> None:
|
||||||
|
run([npm_exe, "install"], cwd=app_root, debug=debug)
|
||||||
|
run([npm_exe, "run", "build"], cwd=app_root, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
def write_service_launcher(app_root: Path, npm_exe: str, port: int) -> Path:
|
||||||
|
if os.name == "nt":
|
||||||
|
launcher = app_root / "run-hydrus-web-gui.cmd"
|
||||||
|
launcher.write_text(
|
||||||
|
"@echo off\n"
|
||||||
|
"setlocal\n"
|
||||||
|
"cd /d \"%~dp0\"\n"
|
||||||
|
f"call \"{npm_exe}\" run build\n"
|
||||||
|
"if errorlevel 1 exit /b %errorlevel%\n"
|
||||||
|
f"call \"{npm_exe}\" run preview -- --host 0.0.0.0 --port {port}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return launcher
|
||||||
|
|
||||||
|
launcher = app_root / "run-hydrus-web-gui.sh"
|
||||||
|
launcher.write_text(
|
||||||
|
"#!/usr/bin/env bash\n"
|
||||||
|
"set -e\n"
|
||||||
|
"cd \"$(dirname \"$0\")\"\n"
|
||||||
|
f"\"{npm_exe}\" run build\n"
|
||||||
|
f"exec \"{npm_exe}\" run preview -- --host 0.0.0.0 --port {port}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
launcher.chmod(launcher.stat().st_mode | 0o111)
|
||||||
|
return launcher
|
||||||
|
|
||||||
|
|
||||||
|
def install_service_windows(service_name: str, launcher_path: Path, debug: bool = False) -> None:
|
||||||
|
schtasks = find_executable("schtasks")
|
||||||
|
if not schtasks:
|
||||||
|
raise RuntimeError("schtasks is required to register the Hydrus web GUI on Windows.")
|
||||||
|
|
||||||
|
tr_command = f'cmd.exe /c ""{launcher_path}""'
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
schtasks,
|
||||||
|
"/Create",
|
||||||
|
"/SC",
|
||||||
|
"ONLOGON",
|
||||||
|
"/TN",
|
||||||
|
service_name,
|
||||||
|
"/TR",
|
||||||
|
tr_command,
|
||||||
|
"/RL",
|
||||||
|
"LIMITED",
|
||||||
|
"/F",
|
||||||
|
],
|
||||||
|
debug=debug,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_service_linux(service_name: str, app_root: Path, launcher_path: Path, debug: bool = False) -> None:
|
||||||
|
systemctl = find_executable("systemctl")
|
||||||
|
if not systemctl:
|
||||||
|
raise RuntimeError("systemctl is required to register the Hydrus web GUI on Linux.")
|
||||||
|
|
||||||
|
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||||
|
unit_dir = Path("/etc/systemd/system")
|
||||||
|
unit_name = f"{service_name}.service"
|
||||||
|
wanted_by = "multi-user.target"
|
||||||
|
systemctl_cmd = [systemctl]
|
||||||
|
service_user = os.environ.get("SUDO_USER") or os.environ.get("USER")
|
||||||
|
else:
|
||||||
|
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
||||||
|
unit_name = f"{service_name}.service"
|
||||||
|
wanted_by = "default.target"
|
||||||
|
systemctl_cmd = [systemctl, "--user"]
|
||||||
|
service_user = None
|
||||||
|
|
||||||
|
unit_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
unit_file = unit_dir / unit_name
|
||||||
|
exec_start = f"/bin/bash -lc {shlex.quote(str(launcher_path))}"
|
||||||
|
|
||||||
|
unit_lines = [
|
||||||
|
"[Unit]",
|
||||||
|
"Description=Hydrus Web GUI",
|
||||||
|
"After=network-online.target",
|
||||||
|
"Wants=network-online.target",
|
||||||
|
"",
|
||||||
|
"[Service]",
|
||||||
|
"Type=simple",
|
||||||
|
f"WorkingDirectory={app_root}",
|
||||||
|
f"ExecStart={exec_start}",
|
||||||
|
"Restart=on-failure",
|
||||||
|
"Environment=NODE_ENV=production",
|
||||||
|
]
|
||||||
|
if service_user and hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||||
|
unit_lines.append(f"User={service_user}")
|
||||||
|
unit_lines.append(f"Group={service_user}")
|
||||||
|
unit_lines.extend([
|
||||||
|
"",
|
||||||
|
"[Install]",
|
||||||
|
f"WantedBy={wanted_by}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
unit_file.write_text("\n".join(unit_lines), encoding="utf-8")
|
||||||
|
|
||||||
|
run([*systemctl_cmd, "daemon-reload"], debug=debug)
|
||||||
|
run([*systemctl_cmd, "enable", "--now", unit_name], debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
def install_service(service_name: str, app_root: Path, launcher_path: Path, debug: bool = False) -> None:
|
||||||
|
system = platform.system().lower()
|
||||||
|
if system == "windows":
|
||||||
|
install_service_windows(service_name, launcher_path, debug=debug)
|
||||||
|
return
|
||||||
|
if system == "linux":
|
||||||
|
install_service_linux(service_name, app_root, launcher_path, debug=debug)
|
||||||
|
return
|
||||||
|
raise RuntimeError("Service installation for the Hydrus web GUI is only supported on Windows and Linux.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_mpv_handler(app_root: Path, npm_exe: str, debug: bool = False) -> None:
|
||||||
|
system = platform.system().lower()
|
||||||
|
if system not in {"windows", "linux"}:
|
||||||
|
print("Skipping mpv-handler setup: this helper is only automated on Windows and Linux.")
|
||||||
|
return
|
||||||
|
run([npm_exe, "run", "setup:mpv-handler"], cwd=app_root, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Install the Hydrus web GUI companion app")
|
||||||
|
parser.add_argument("--root", default=".", help="Root directory that will contain the checkout")
|
||||||
|
parser.add_argument("--dest-name", default=DEFAULT_DEST_NAME, help="Folder name for the checkout")
|
||||||
|
parser.add_argument("--repo", default=DEFAULT_REPO_URL, help="Repository URL for the Hydrus web GUI")
|
||||||
|
parser.add_argument("--force", action="store_true", help="Replace an existing non-empty destination")
|
||||||
|
parser.add_argument(
|
||||||
|
"--install-service",
|
||||||
|
action="store_true",
|
||||||
|
help="Register the web GUI to start automatically using Task Scheduler or systemd",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--service-name",
|
||||||
|
default=DEFAULT_SERVICE_NAME,
|
||||||
|
help=f"Service name to register (default: {DEFAULT_SERVICE_NAME})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--setup-mpv-handler",
|
||||||
|
action="store_true",
|
||||||
|
help="Run the web GUI's mpv-handler setup helper after installation",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=DEFAULT_PORT,
|
||||||
|
help=f"Preferred preview port for the generated service launcher (default: {DEFAULT_PORT})",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Show installer debug output")
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
root = Path(os.path.expandvars(os.path.expanduser(args.root))).resolve()
|
||||||
|
dest = root / os.path.expandvars(args.dest_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, npm_exe = ensure_node_runtime(debug=args.debug)
|
||||||
|
clone_or_update_repo(args.repo, dest, force=args.force, debug=args.debug)
|
||||||
|
install_web_gui_dependencies(dest, npm_exe, debug=args.debug)
|
||||||
|
launcher_path = write_service_launcher(dest, npm_exe, args.port)
|
||||||
|
|
||||||
|
if args.setup_mpv_handler:
|
||||||
|
setup_mpv_handler(dest, npm_exe, debug=args.debug)
|
||||||
|
|
||||||
|
if args.install_service:
|
||||||
|
install_service(args.service_name, dest, launcher_path, debug=args.debug)
|
||||||
|
|
||||||
|
print(f"Hydrus web GUI ready at: {dest}")
|
||||||
|
print(f"Launcher: {launcher_path}")
|
||||||
|
print(f"URL: http://localhost:{args.port}")
|
||||||
|
return 0
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
print(f"Hydrus web GUI install failed: {exc}", file=sys.stderr)
|
||||||
|
return int(exc.returncode or 1)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Hydrus web GUI install error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user