mirror of
https://github.com/dw-0/kiauh.git
synced 2025-10-25 23:46:07 +02:00
feat(extension): add Spoolman Docker installer (#669)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
@@ -11,5 +11,5 @@ end_of_line = lf
|
||||
[*.py]
|
||||
max_line_length = 88
|
||||
|
||||
[*.{sh,yml,yaml}]
|
||||
[*.{sh,yml,yaml,json}]
|
||||
indent_size = 2
|
||||
@@ -386,7 +386,7 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
|
||||
self.flash_options.selected_baudrate = get_number_input(
|
||||
question="Please set the baud rate",
|
||||
default=250000,
|
||||
min_count=0,
|
||||
min_value=0,
|
||||
allow_go_back=True,
|
||||
)
|
||||
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||
|
||||
@@ -414,7 +414,7 @@ def get_client_port_selection(
|
||||
while True:
|
||||
_type = "Reconfigure" if reconfigure else "Configure"
|
||||
question = f"{_type} {client.display_name} for port"
|
||||
port_input = get_number_input(question, min_count=80, default=port)
|
||||
port_input = get_number_input(question, min_value=80, default=port)
|
||||
|
||||
if port_input not in ports_in_use:
|
||||
client_settings: WebUiSettings = settings[client.name]
|
||||
|
||||
@@ -43,7 +43,7 @@ class PrettyGcodeExtension(BaseExtension):
|
||||
|
||||
port = get_number_input(
|
||||
"On which port should PrettyGCode run",
|
||||
min_count=0,
|
||||
min_value=0,
|
||||
default=7136,
|
||||
allow_go_back=True,
|
||||
)
|
||||
|
||||
16
kiauh/extensions/spoolman/__init__.py
Normal file
16
kiauh/extensions/spoolman/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
from pathlib import Path
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parent
|
||||
SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest"
|
||||
SPOOLMAN_DIR = Path.home().joinpath("spoolman")
|
||||
SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data")
|
||||
SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml")
|
||||
SPOOLMAN_DEFAULT_PORT = 7912
|
||||
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
spoolman:
|
||||
image: ghcr.io/donkie/spoolman:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
|
||||
- type: bind
|
||||
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
|
||||
target: /home/app/.local/share/spoolman # Do NOT modify this line
|
||||
ports:
|
||||
# Map the host machine's port 7912 to the container's port 8000
|
||||
- "7912:8000"
|
||||
environment:
|
||||
- TZ=Europe/Stockholm # Optional, defaults to UTC
|
||||
18
kiauh/extensions/spoolman/metadata.json
Normal file
18
kiauh/extensions/spoolman/metadata.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"metadata": {
|
||||
"index": 11,
|
||||
"module": "spoolman_extension",
|
||||
"maintained_by": "dw-0",
|
||||
"display_name": "Spoolman (Docker)",
|
||||
"description": [
|
||||
"Filament manager for 3D printing",
|
||||
"- Track your filament inventory",
|
||||
"- Monitor filament usage",
|
||||
"- Manage vendors, materials, and spools",
|
||||
"- Integrates with Moonraker",
|
||||
"\n\n",
|
||||
"Note: This extension installs Spoolman using Docker. Docker must be installed on your system before installing Spoolman."
|
||||
],
|
||||
"updates": true
|
||||
}
|
||||
}
|
||||
190
kiauh/extensions/spoolman/spoolman.py
Normal file
190
kiauh/extensions/spoolman/spoolman.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from core.instance_manager.base_instance import BaseInstance
|
||||
from core.logger import Logger
|
||||
from extensions.spoolman import (
|
||||
MODULE_PATH,
|
||||
SPOOLMAN_COMPOSE_FILE,
|
||||
SPOOLMAN_DIR,
|
||||
SPOOLMAN_DOCKER_IMAGE,
|
||||
)
|
||||
from utils.sys_utils import get_system_timezone
|
||||
|
||||
|
||||
@dataclass
|
||||
class Spoolman:
|
||||
suffix: str
|
||||
base: BaseInstance = field(init=False, repr=False)
|
||||
dir: Path = SPOOLMAN_DIR
|
||||
data_dir: Path = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
|
||||
self.data_dir = self.base.data_dir
|
||||
|
||||
@staticmethod
|
||||
def is_container_running() -> bool:
|
||||
"""Check if the Spoolman container is running"""
|
||||
try:
|
||||
result = run(
|
||||
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "ps", "-q"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return bool(result.stdout.strip())
|
||||
except CalledProcessError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_docker_available() -> bool:
|
||||
"""Check if Docker is installed and available"""
|
||||
try:
|
||||
run(["docker", "--version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_docker_compose_available() -> bool:
|
||||
"""Check if Docker Compose is installed and available"""
|
||||
try:
|
||||
# Try modern docker compose command
|
||||
run(["docker", "compose", "version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
# Try legacy docker-compose command
|
||||
try:
|
||||
run(["docker-compose", "--version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def create_docker_compose() -> bool:
|
||||
"""Copy the docker-compose.yml file for Spoolman and set system timezone"""
|
||||
try:
|
||||
shutil.copy(
|
||||
MODULE_PATH.joinpath("assets/docker-compose.yml"),
|
||||
SPOOLMAN_COMPOSE_FILE,
|
||||
)
|
||||
|
||||
# get system timezone
|
||||
timezone = get_system_timezone()
|
||||
|
||||
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace("TZ=Europe/Stockholm", f"TZ={timezone}")
|
||||
|
||||
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error creating Docker Compose file: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def start_container() -> bool:
|
||||
"""Start the Spoolman container"""
|
||||
try:
|
||||
run(
|
||||
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "up", "-d"],
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Failed to start Spoolman container: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def update_container() -> bool:
|
||||
"""Update the Spoolman container"""
|
||||
|
||||
def __get_image_id() -> str:
|
||||
"""Get the image ID of the Spoolman Docker image"""
|
||||
try:
|
||||
result = run(
|
||||
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except CalledProcessError:
|
||||
raise Exception("Failed to get Spoolman Docker image ID")
|
||||
|
||||
try:
|
||||
old_image_id = __get_image_id()
|
||||
Logger.print_status("Pulling latest Spoolman image...")
|
||||
Spoolman.pull_image()
|
||||
new_image_id = __get_image_id()
|
||||
Logger.print_status("Tearing down old Spoolman container...")
|
||||
Spoolman.tear_down_container()
|
||||
Logger.print_status("Spinning up new Spoolman container...")
|
||||
Spoolman.start_container()
|
||||
if old_image_id != new_image_id:
|
||||
Logger.print_status("Removing old Spoolman image...")
|
||||
run(["docker", "rmi", old_image_id], check=True)
|
||||
return True
|
||||
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Failed to update Spoolman container: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def tear_down_container() -> bool:
|
||||
"""Stop and remove the Spoolman container"""
|
||||
try:
|
||||
run(
|
||||
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "down"],
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Failed to tear down Spoolman container: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def pull_image() -> bool:
|
||||
"""Pull the Spoolman Docker image"""
|
||||
try:
|
||||
run(["docker", "pull", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Failed to pull Spoolman Docker image: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def remove_image() -> bool:
|
||||
"""Remove the Spoolman Docker image"""
|
||||
try:
|
||||
image_exists = run(
|
||||
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if not image_exists:
|
||||
Logger.print_info("Spoolman Docker image not found. Nothing to remove.")
|
||||
return False
|
||||
|
||||
run(["docker", "rmi", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||
return True
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Failed to remove Spoolman Docker image: {e}")
|
||||
return False
|
||||
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
|
||||
import re
|
||||
from subprocess import CalledProcessError, run
|
||||
from typing import List, Tuple
|
||||
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from components.moonraker.services.moonraker_instance_service import (
|
||||
MoonrakerInstanceService,
|
||||
)
|
||||
from core.backup_manager.backup_manager import BackupManager
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.logger import DialogType, Logger
|
||||
from extensions.base_extension import BaseExtension
|
||||
from extensions.spoolman import (
|
||||
SPOOLMAN_COMPOSE_FILE,
|
||||
SPOOLMAN_DATA_DIR,
|
||||
SPOOLMAN_DEFAULT_PORT,
|
||||
SPOOLMAN_DIR,
|
||||
)
|
||||
from extensions.spoolman.spoolman import Spoolman
|
||||
from utils.config_utils import (
|
||||
add_config_section,
|
||||
remove_config_section,
|
||||
)
|
||||
from utils.fs_utils import run_remove_routines
|
||||
from utils.input_utils import get_confirm, get_number_input
|
||||
from utils.sys_utils import get_ipv4_addr
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class SpoolmanExtension(BaseExtension):
|
||||
ip: str = ""
|
||||
port: int = SPOOLMAN_DEFAULT_PORT
|
||||
|
||||
def install_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Installing Spoolman using Docker...")
|
||||
|
||||
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||
if not docker_available or not docker_compose_available:
|
||||
return
|
||||
|
||||
if not self.__handle_existing_installation():
|
||||
self.ip: str = get_ipv4_addr()
|
||||
self.__run_setup()
|
||||
|
||||
# noinspection HttpUrlsUsage
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
[
|
||||
"Spoolman successfully installed using Docker!",
|
||||
"You can access Spoolman via the following URL:",
|
||||
f"http://{self.ip}:{self.port}",
|
||||
],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
def update_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Updating Spoolman Docker container...")
|
||||
|
||||
if not SPOOLMAN_DIR.exists() or not SPOOLMAN_COMPOSE_FILE.exists():
|
||||
Logger.print_error("Spoolman installation not found or incomplete.")
|
||||
return
|
||||
|
||||
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||
if not docker_available or not docker_compose_available:
|
||||
return
|
||||
|
||||
Logger.print_status("Updating Spoolman container...")
|
||||
if not Spoolman.update_container():
|
||||
return
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
["Spoolman Docker container successfully updated!"],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
def remove_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Removing Spoolman Docker container...")
|
||||
|
||||
if not SPOOLMAN_DIR.exists():
|
||||
Logger.print_info("Spoolman is not installed. Nothing to remove.")
|
||||
return
|
||||
|
||||
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||
if not docker_available or not docker_compose_available:
|
||||
return
|
||||
|
||||
# remove moonraker integration
|
||||
mrsvc = MoonrakerInstanceService()
|
||||
mrsvc.load_instances()
|
||||
mr_instances: List[Moonraker] = mrsvc.get_all_instances()
|
||||
|
||||
Logger.print_status("Removing Spoolman configuration from moonraker.conf...")
|
||||
remove_config_section("spoolman", mr_instances)
|
||||
|
||||
Logger.print_status("Removing Spoolman from moonraker.asvc...")
|
||||
self.__remove_from_moonraker_asvc()
|
||||
|
||||
# stop and remove the container if docker-compose exists
|
||||
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||
Logger.print_status("Stopping and removing Spoolman container...")
|
||||
|
||||
if Spoolman.tear_down_container():
|
||||
Logger.print_ok("Spoolman container removed!")
|
||||
else:
|
||||
Logger.print_error(
|
||||
"Failed to remove Spoolman container! Please remove it manually."
|
||||
)
|
||||
|
||||
if Spoolman.remove_image():
|
||||
Logger.print_ok("Spoolman container and image removed!")
|
||||
else:
|
||||
Logger.print_error(
|
||||
"Failed to remove Spoolman image! Please remove it manually."
|
||||
)
|
||||
|
||||
# backup Spoolman directory to ~/spoolman_data-<timestamp> before removing it
|
||||
try:
|
||||
bm = BackupManager()
|
||||
result = bm.backup_directory(
|
||||
f"{SPOOLMAN_DIR.name}_data",
|
||||
source=SPOOLMAN_DIR,
|
||||
target=SPOOLMAN_DIR.parent,
|
||||
)
|
||||
if result:
|
||||
Logger.print_ok(f"Spoolman data backed up to {result}")
|
||||
Logger.print_status("Removing Spoolman directory...")
|
||||
if run_remove_routines(SPOOLMAN_DIR):
|
||||
Logger.print_ok("Spoolman directory removed!")
|
||||
else:
|
||||
Logger.print_error(
|
||||
"Failed to remove Spoolman directory! Please remove it manually."
|
||||
)
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Failed to backup Spoolman directory: {e}")
|
||||
Logger.print_info("Skipping Spoolman directory removal...")
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
["Spoolman successfully removed!"],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
def __run_setup(self) -> None:
|
||||
# Create Spoolman directory and data directory
|
||||
Logger.print_status("Setting up Spoolman directories...")
|
||||
SPOOLMAN_DIR.mkdir(parents=True)
|
||||
Logger.print_ok(f"Directory {SPOOLMAN_DIR} created!")
|
||||
SPOOLMAN_DATA_DIR.mkdir(parents=True)
|
||||
Logger.print_ok(f"Directory {SPOOLMAN_DATA_DIR} created!")
|
||||
|
||||
# Set correct permissions for data directory
|
||||
try:
|
||||
Logger.print_status("Setting permissions for Spoolman data directory...")
|
||||
run(["chown", "1000:1000", str(SPOOLMAN_DATA_DIR)], check=True)
|
||||
Logger.print_ok("Permissions set!")
|
||||
except CalledProcessError:
|
||||
Logger.print_warn(
|
||||
"Could not set permissions on data directory. This might cause issues."
|
||||
)
|
||||
|
||||
Logger.print_status("Creating Docker Compose file...")
|
||||
if Spoolman.create_docker_compose():
|
||||
Logger.print_ok("Docker Compose file created!")
|
||||
else:
|
||||
Logger.print_error("Failed to create Docker Compose file!")
|
||||
|
||||
self.__port_config_prompt()
|
||||
|
||||
Logger.print_status("Spinning up Spoolman container...")
|
||||
if Spoolman.start_container():
|
||||
Logger.print_ok("Spoolman container started!")
|
||||
else:
|
||||
Logger.print_error("Failed to start Spoolman container!")
|
||||
|
||||
if self.__add_moonraker_integration():
|
||||
Logger.print_ok("Spoolman integration added to Moonraker!")
|
||||
else:
|
||||
Logger.print_info("Moonraker integration skipped.")
|
||||
|
||||
def __check_docker_prereqs(self) -> Tuple[bool, bool]:
|
||||
# check if Docker is available
|
||||
is_docker_available = Spoolman.is_docker_available()
|
||||
if not is_docker_available:
|
||||
Logger.print_error("Docker is not installed or not available.")
|
||||
Logger.print_info(
|
||||
"Please install Docker first: https://docs.docker.com/engine/install/"
|
||||
)
|
||||
|
||||
# check if Docker Compose is available
|
||||
is_docker_compose_available = Spoolman.is_docker_compose_available()
|
||||
if not is_docker_compose_available:
|
||||
Logger.print_error("Docker Compose is not installed or not available.")
|
||||
|
||||
return is_docker_available, is_docker_compose_available
|
||||
|
||||
def __port_config_prompt(self) -> None:
|
||||
"""Prompt for advanced configuration options"""
|
||||
Logger.print_dialog(
|
||||
DialogType.INFO,
|
||||
[
|
||||
"You can configure Spoolman to run on a different port than the default. "
|
||||
"Make sure you don't select a port which is already in use by "
|
||||
"another application. Your input will not be validated! "
|
||||
"The default port is 7912.",
|
||||
],
|
||||
)
|
||||
if not get_confirm("Continue with default port 7912?", default_choice=True):
|
||||
self.__set_port()
|
||||
|
||||
def __set_port(self) -> None:
|
||||
"""Configure advanced options for Spoolman Docker container"""
|
||||
port = get_number_input(
|
||||
"Which port should Spoolman run on?",
|
||||
default=SPOOLMAN_DEFAULT_PORT,
|
||||
min_value=1024,
|
||||
max_value=65535,
|
||||
)
|
||||
|
||||
if port != SPOOLMAN_DEFAULT_PORT:
|
||||
self.port = port
|
||||
|
||||
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
port_mapping_pattern = r'"(\d+):8000"'
|
||||
content = re.sub(port_mapping_pattern, f'"{port}:8000"', content)
|
||||
|
||||
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
Logger.print_ok(f"Port set to {port}...")
|
||||
|
||||
def __handle_existing_installation(self) -> bool:
|
||||
if not (SPOOLMAN_DIR.exists() and SPOOLMAN_DIR.is_dir()):
|
||||
return False
|
||||
|
||||
compose_file_exists = SPOOLMAN_COMPOSE_FILE.exists()
|
||||
container_running = Spoolman.is_container_running()
|
||||
|
||||
if container_running and compose_file_exists:
|
||||
Logger.print_info("Spoolman is already installed!")
|
||||
return True
|
||||
elif container_running and not compose_file_exists:
|
||||
Logger.print_status(
|
||||
"Spoolman container is running but Docker Compose file is missing..."
|
||||
)
|
||||
if get_confirm(
|
||||
"Do you want to recreate the Docker Compose file?",
|
||||
default_choice=True,
|
||||
):
|
||||
Spoolman.create_docker_compose()
|
||||
self.__port_config_prompt()
|
||||
return True
|
||||
elif not container_running and compose_file_exists:
|
||||
Logger.print_status(
|
||||
"Docker Compose file exists but container is not running..."
|
||||
)
|
||||
Spoolman.start_container()
|
||||
return True
|
||||
return False
|
||||
|
||||
def __add_moonraker_integration(self) -> bool:
|
||||
"""Enable Moonraker integration for Spoolman Docker container"""
|
||||
if not get_confirm("Add Moonraker integration?", default_choice=True):
|
||||
return False
|
||||
|
||||
Logger.print_status("Adding Spoolman integration to Moonraker...")
|
||||
|
||||
# read port from the docker-compose file
|
||||
port = SPOOLMAN_DEFAULT_PORT
|
||||
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||
content = f.read()
|
||||
# Extract port from the port mapping
|
||||
port_match = re.search(r'"(\d+):8000"', content)
|
||||
if port_match:
|
||||
port = port_match.group(1)
|
||||
|
||||
mrsvc = MoonrakerInstanceService()
|
||||
mrsvc.load_instances()
|
||||
mr_instances = mrsvc.get_all_instances()
|
||||
|
||||
# noinspection HttpUrlsUsage
|
||||
add_config_section(
|
||||
section="spoolman",
|
||||
instances=mr_instances,
|
||||
options=[("server", f"http://{self.ip}:{port}")],
|
||||
)
|
||||
|
||||
Logger.print_status("Adding Spoolman to moonraker.asvc...")
|
||||
self.__add_to_moonraker_asvc()
|
||||
|
||||
InstanceManager.restart_all(mr_instances)
|
||||
|
||||
return True
|
||||
|
||||
def __add_to_moonraker_asvc(self) -> None:
|
||||
"""Add Spoolman to moonraker.asvc"""
|
||||
mrsvc = MoonrakerInstanceService()
|
||||
mrsvc.load_instances()
|
||||
mr_instances = mrsvc.get_all_instances()
|
||||
for instance in mr_instances:
|
||||
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||
if asvc_path.exists():
|
||||
if "Spoolman" in open(asvc_path).read():
|
||||
Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...")
|
||||
continue
|
||||
|
||||
with open(asvc_path, "a") as f:
|
||||
f.write("Spoolman\n")
|
||||
|
||||
Logger.print_ok(f"Spoolman added to {asvc_path}!")
|
||||
|
||||
def __remove_from_moonraker_asvc(self) -> None:
|
||||
"""Remove Spoolman from moonraker.asvc"""
|
||||
mrsvc = MoonrakerInstanceService()
|
||||
mrsvc.load_instances()
|
||||
mr_instances = mrsvc.get_all_instances()
|
||||
for instance in mr_instances:
|
||||
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||
if asvc_path.exists():
|
||||
if "Spoolman" not in open(asvc_path).read():
|
||||
Logger.print_info(f"Spoolman not in {asvc_path}. Skipping...")
|
||||
continue
|
||||
|
||||
with open(asvc_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = [line for line in lines if "Spoolman" not in line]
|
||||
|
||||
with open(asvc_path, "w") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
Logger.print_ok(f"Spoolman removed from {asvc_path}!")
|
||||
@@ -52,16 +52,16 @@ def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool
|
||||
|
||||
def get_number_input(
|
||||
question: str,
|
||||
min_count: int,
|
||||
max_count: int | None = None,
|
||||
min_value: int,
|
||||
max_value: int | None = None,
|
||||
default: int | None = None,
|
||||
allow_go_back: bool = False,
|
||||
) -> int | None:
|
||||
"""
|
||||
Helper method to get a number input from the user
|
||||
:param question: The question to display
|
||||
:param min_count: The lowest allowed value
|
||||
:param max_count: The highest allowed value (or None)
|
||||
:param min_value: The lowest allowed value
|
||||
:param max_value: The highest allowed value (or None)
|
||||
:param default: Optional default value
|
||||
:param allow_go_back: Navigate back to a previous dialog
|
||||
:return: Either the validated number input, or None on go_back
|
||||
@@ -77,7 +77,7 @@ def get_number_input(
|
||||
return default
|
||||
|
||||
try:
|
||||
return validate_number_input(_input, min_count, max_count)
|
||||
return validate_number_input(_input, min_value, max_value)
|
||||
except ValueError:
|
||||
Logger.print_error(INVALID_CHOICE)
|
||||
|
||||
|
||||
@@ -359,11 +359,12 @@ def get_ipv4_addr() -> str:
|
||||
try:
|
||||
# doesn't even have to be reachable
|
||||
s.connect(("192.255.255.255", 1))
|
||||
return str(s.getsockname()[0])
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
finally:
|
||||
ipv4: str = str(s.getsockname()[0])
|
||||
s.close()
|
||||
return ipv4
|
||||
except Exception:
|
||||
s.close()
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def download_file(url: str, target: Path, show_progress=True) -> None:
|
||||
@@ -600,3 +601,33 @@ def get_distro_info() -> Tuple[str, str]:
|
||||
raise ValueError("Error reading distro version!")
|
||||
|
||||
return distro_id.lower(), distro_version
|
||||
|
||||
|
||||
def get_system_timezone() -> str:
|
||||
timezone = "UTC"
|
||||
try:
|
||||
with open("/etc/timezone", "r") as f:
|
||||
timezone = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
# fallback to reading timezone from timedatectl
|
||||
try:
|
||||
result = run(
|
||||
["timedatectl", "show", "--property=Timezone"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
timezone = result.stdout.strip().split("=")[1]
|
||||
except CalledProcessError:
|
||||
# fallback if timedatectl fails, try reading from readlink
|
||||
try:
|
||||
result = run(
|
||||
["readlink", "-f", "/etc/localtime"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
timezone = result.stdout.strip().split("zoneinfo/")[1]
|
||||
except (CalledProcessError, IndexError):
|
||||
Logger.print_warn("Could not determine system timezone, using UTC")
|
||||
return timezone
|
||||
|
||||
Reference in New Issue
Block a user