diff --git a/README.md b/README.md index e507a80..4c7b1ee 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,10 @@ -# 🎬 Zordon - Render Management Tools 🎬 +# 🎬 Zordon - Render Management Tools -Welcome to Zordon! This is a hobby project written with fellow filmmakers in mind. It's a local network render farm manager, aiming to streamline and simplify the rendering process across multiple home computers. +Welcome to Zordon! It's a local network render farm manager, aiming to streamline and simplify the rendering process across multiple home computers. ## 📦 Installation -Make sure to install the necessary dependencies: `pip3 install -r requirements.txt` - -## 🚀 How to Use - -Zordon has two main files: `start_server.py` and `start_client.py`. - -- **start_server.py**: Run this on any computer you want to render jobs. It manages the incoming job queue and kicks off the appropriate render jobs when ready. -- **start_client.py**: Run this to administer your render servers. It lets you manage and submit jobs. - -When the server is running, the job queue can be accessed via a web browser on the server's hostname (default port is 8080). You can also access it via the GUI client or a simple view-only dashboard. +Install the necessary dependencies: `pip3 install -r requirements.txt` ## 🎨 Supported Renderers diff --git a/requirements.txt b/requirements.txt index d496de6..845e035 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,35 @@ -requests==2.31.0 -psutil==5.9.6 -PyYAML==6.0.1 -Flask==3.0.0 -rich==13.6.0 -Werkzeug~=3.0.1 -json2html~=1.3.0 -SQLAlchemy~=2.0.15 -Pillow==10.1.0 -zeroconf==0.119.0 -Pypubsub~=4.0.3 -tqdm==4.66.1 -plyer==2.1.0 -PyQt6~=6.6.0 -PySide6~=6.6.0 \ No newline at end of file +PyQt6>=6.6.1 +psutil>=5.9.8 +requests>=2.31.0 +Pillow>=10.2.0 +json2html>=1.3.0 +PyYAML>=6.0.1 +flask>=3.0.1 +tqdm>=4.66.1 +werkzeug>=3.0.1 +Pypubsub>=4.0.3 +zeroconf>=0.131.0 +SQLAlchemy>=2.0.25 +plyer>=2.1.0 +pytz>=2023.3.post1 +future>=0.18.3 +rich>=13.7.0 +pytest>=8.0.0 +numpy>=1.26.3 +setuptools>=69.0.3 +pandas>=2.2.0 +matplotlib>=3.8.2 +MarkupSafe>=2.1.4 +python-dateutil>=2.8.2 +certifi>=2023.11.17 +PySide6>=6.6.1 +shiboken6>=6.6.1 +Pygments>=2.17.2 +cycler>=0.12.1 +contourpy>=1.2.0 +packaging>=23.2 +fonttools>=4.47.2 +Jinja2>=3.1.3 +pyparsing>=3.1.1 +kiwisolver>=1.4.5 +attrs>=23.2.0 \ No newline at end of file diff --git a/src/api/add_job_helpers.py b/src/api/add_job_helpers.py index 37f5870..fc7d910 100644 --- a/src/api/add_job_helpers.py +++ b/src/api/add_job_helpers.py @@ -86,7 +86,6 @@ def download_project_from_url(project_url): # This nested function is to handle downloading from a URL logger.info(f"Downloading project from url: {project_url}") referred_name = os.path.basename(project_url) - downloaded_file_url = None try: response = requests.get(project_url, stream=True) @@ -156,7 +155,7 @@ def process_zipped_project(zip_path): return extracted_project_path -def create_render_jobs(jobs_list, loaded_project_local_path, job_dir): +def create_render_jobs(jobs_list, loaded_project_local_path): """ Creates render jobs. @@ -166,7 +165,6 @@ def create_render_jobs(jobs_list, loaded_project_local_path, job_dir): Args: jobs_list (list): A list of job data. loaded_project_local_path (str): The local path to the loaded project. - job_dir (str): The job directory. Returns: list: A list of results from creating the render jobs. diff --git a/src/api/api_server.py b/src/api/api_server.py index 916e89f..e855043 100755 --- a/src/api/api_server.py +++ b/src/api/api_server.py @@ -181,7 +181,7 @@ def make_job_ready(job_id): RenderQueue.save_state() return found_job.json(), 200 except Exception as e: - return "Error making job ready: {e}", 500 + return f"Error making job ready: {e}", 500 return "Not valid command", 405 @@ -213,8 +213,8 @@ def download_all(job_id): def presets(): presets_path = system_safe_path('config/presets.yaml') with open(presets_path) as f: - presets = yaml.load(f, Loader=yaml.FullLoader) - return presets + loaded_presets = yaml.load(f, Loader=yaml.FullLoader) + return loaded_presets @server.get('/api/full_status') @@ -268,7 +268,7 @@ def add_job_handler(): if loaded_project_local_path.lower().endswith('.zip'): loaded_project_local_path = process_zipped_project(loaded_project_local_path) - results = create_render_jobs(jobs_list, loaded_project_local_path, referred_name) + results = create_render_jobs(jobs_list, loaded_project_local_path) for response in results: if response.get('error', None): return results, 400 @@ -376,7 +376,8 @@ def renderer_info(): if installed_versions: # fixme: using system versions only because downloaded versions may have permissions issues system_installed_versions = [x for x in installed_versions if x['type'] == 'system'] - install_path = system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path'] + install_path = system_installed_versions[0]['path'] if system_installed_versions else ( + installed_versions)[0]['path'] renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()), 'versions': installed_versions, 'supported_extensions': engine.supported_extensions(), @@ -482,7 +483,7 @@ def start_server(): flask_log = logging.getLogger('werkzeug') flask_log.setLevel(Config.flask_log_level.upper()) - # check for updates for render engines if config'd or on first launch + # check for updates for render engines if configured or on first launch if Config.update_engines_on_launch or not EngineManager.all_engines(): EngineManager.update_all_engines() diff --git a/src/api/serverproxy_manager.py b/src/api/serverproxy_manager.py index 5cb48b4..dcd4d7b 100644 --- a/src/api/serverproxy_manager.py +++ b/src/api/serverproxy_manager.py @@ -32,4 +32,3 @@ class ServerProxyManager: cls.server_proxys[hostname] = new_proxy found_proxy = new_proxy return found_proxy - diff --git a/src/distributed_job_manager.py b/src/distributed_job_manager.py index 8f74da4..28b3cf4 100644 --- a/src/distributed_job_manager.py +++ b/src/distributed_job_manager.py @@ -221,8 +221,9 @@ class DistributedJobManager: """ Splits a job into subjobs and distributes them among available servers. - This method checks the availability of servers, distributes the work among them, and creates subjobs on each server. - If a server is the local host, it adjusts the frame range of the parent job instead of creating a subjob. + This method checks the availability of servers, distributes the work among them, and creates subjobs on each + server. If a server is the local host, it adjusts the frame range of the parent job instead of creating a + subjob. Args: worker (Worker): The worker that is handling the job. @@ -303,8 +304,8 @@ class DistributedJobManager: Defaults to 'cpu_count'. Returns: - list: A list of server dictionaries where each dictionary includes the frame range and total number of frames - to be rendered by the server. + list: A list of server dictionaries where each dictionary includes the frame range and total number of + frames to be rendered by the server. """ # Calculate respective frames for each server def divide_frames_by_cpu_count(frame_start, frame_end, servers): diff --git a/src/engines/blender/blender_downloader.py b/src/engines/blender/blender_downloader.py index 83ba6c5..86c5231 100644 --- a/src/engines/blender/blender_downloader.py +++ b/src/engines/blender/blender_downloader.py @@ -15,7 +15,6 @@ supported_formats = ['.zip', '.tar.xz', '.dmg'] class BlenderDownloader(EngineDownloader): - engine = Blender @staticmethod @@ -43,11 +42,13 @@ class BlenderDownloader(EngineDownloader): response = requests.get(base_url, timeout=5) response.raise_for_status() - versions_pattern = r'blender-(?P[\d\.]+)-(?P\w+)-(?P\w+).*' + versions_pattern = \ + r'blender-(?P[\d\.]+)-(?P\w+)-(?P\w+).*' versions_data = [match.groupdict() for match in re.finditer(versions_pattern, response.text)] # Filter to just the supported formats - versions_data = [item for item in versions_data if any(item["file"].endswith(ext) for ext in supported_formats)] + versions_data = [item for item in versions_data if any(item["file"].endswith(ext) for ext in + supported_formats)] # Filter down OS and CPU system_os = system_os or current_system_os() @@ -105,11 +106,12 @@ class BlenderDownloader(EngineDownloader): try: logger.info(f"Requesting download of blender-{version}-{system_os}-{cpu}") major_version = '.'.join(version.split('.')[:2]) - minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if x['version'] == version] + minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if + x['version'] == version] # we get the URL instead of calculating it ourselves. May change this cls.download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location, - timeout=timeout) + timeout=timeout) except IndexError: logger.error("Cannot find requested engine") @@ -117,5 +119,4 @@ class BlenderDownloader(EngineDownloader): if __name__ == '__main__': logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - print(BlenderDownloader.__get_major_versions()) - + print(BlenderDownloader.find_most_recent_version()) diff --git a/src/engines/core/base_downloader.py b/src/engines/core/base_downloader.py index 4867f01..356edea 100644 --- a/src/engines/core/base_downloader.py +++ b/src/engines/core/base_downloader.py @@ -98,7 +98,7 @@ class EngineDownloader: zip_ref.extractall(download_location) logger.info( f'Successfully extracted {os.path.basename(temp_downloaded_file_path)} to {download_location}') - except zipfile.BadZipFile as e: + except zipfile.BadZipFile: logger.error(f'Error: {temp_downloaded_file_path} is not a valid ZIP file.') except FileNotFoundError: logger.error(f'File not found: {temp_downloaded_file_path}') @@ -110,7 +110,8 @@ class EngineDownloader: for mount_point in dmg.attach(): try: copy_directory_contents(mount_point, os.path.join(download_location, output_dir_name)) - logger.info(f'Successfully copied {os.path.basename(temp_downloaded_file_path)} to {download_location}') + logger.info(f'Successfully copied {os.path.basename(temp_downloaded_file_path)} ' + f'to {download_location}') except FileNotFoundError: logger.error(f'Error: The source .app bundle does not exist.') except PermissionError: diff --git a/src/engines/core/base_engine.py b/src/engines/core/base_engine.py index 92da42e..639354b 100644 --- a/src/engines/core/base_engine.py +++ b/src/engines/core/base_engine.py @@ -71,4 +71,3 @@ class BaseRenderEngine(object): def perform_presubmission_tasks(self, project_path): return project_path - diff --git a/src/engines/core/base_worker.py b/src/engines/core/base_worker.py index c3fa603..7ef1ba8 100644 --- a/src/engines/core/base_worker.py +++ b/src/engines/core/base_worker.py @@ -227,7 +227,8 @@ class BaseRenderWorker(Base): return if not return_code: - message = f"{'=' * 50}\n\n{self.engine.name()} render completed successfully in {self.time_elapsed()}" + message = (f"{'=' * 50}\n\n{self.engine.name()} render completed successfully in " + f"{self.time_elapsed()}") f.write(message) break diff --git a/src/engines/engine_manager.py b/src/engines/engine_manager.py index 9edf4b6..c812b4a 100644 --- a/src/engines/engine_manager.py +++ b/src/engines/engine_manager.py @@ -86,7 +86,8 @@ class EngineManager: cpu = cpu or current_system_cpu() try: - filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and x['cpu'] == cpu] + filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and + x['cpu'] == cpu] return filtered[0] except IndexError: logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}") @@ -98,7 +99,8 @@ class EngineManager: cpu = cpu or current_system_cpu() filtered = [x for x in cls.all_engines() if - x['engine'] == engine and x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version] + x['engine'] == engine and x['system_os'] == system_os and x['cpu'] == cpu and + x['version'] == version] return filtered[0] if filtered else False @classmethod @@ -107,6 +109,7 @@ class EngineManager: downloader = cls.engine_with_name(engine).downloader() return downloader.version_is_available_to_download(version=version, system_os=system_os, cpu=cpu) except Exception as e: + logger.debug(f"Exception in version_is_available_to_download: {e}") return None @classmethod @@ -115,10 +118,11 @@ class EngineManager: downloader = cls.engine_with_name(engine).downloader() return downloader.find_most_recent_version(system_os=system_os, cpu=cpu) except Exception as e: + logger.debug(f"Exception in find_most_recent_version: {e}") return None @classmethod - def is_already_downloading(cls, engine, version, system_os=None, cpu=None): + def get_existing_download_task(cls, engine, version, system_os=None, cpu=None): for task in cls.download_tasks: task_parts = task.name.split('-') task_engine, task_version, task_system_os, task_cpu = task_parts[:4] @@ -126,17 +130,17 @@ class EngineManager: if engine == task_engine and version == task_version: if system_os in (task_system_os, None) and cpu in (task_cpu, None): return task - return False + return None @classmethod def download_engine(cls, engine, version, system_os=None, cpu=None, background=False): engine_to_download = cls.engine_with_name(engine) - existing_task = cls.is_already_downloading(engine, version, system_os, cpu) + existing_task = cls.get_existing_download_task(engine, version, system_os, cpu) if existing_task: logger.debug(f"Already downloading {engine} {version}") if not background: - existing_task.join() # If download task exists, wait until its done downloading + existing_task.join() # If download task exists, wait until it's done downloading return elif not engine_to_download.downloader(): logger.warning("No valid downloader for this engine. Please update this software manually.") @@ -157,7 +161,6 @@ class EngineManager: logger.error(f"Error downloading {engine}") return found_engine - @classmethod def delete_engine_download(cls, engine, version, system_os=None, cpu=None): logger.info(f"Requested deletion of engine: {engine}-{version}") @@ -180,14 +183,14 @@ class EngineManager: @classmethod def update_all_engines(cls): - def engine_update_task(engine): - logger.debug(f"Checking for updates to {engine.name()}") - latest_version = engine.downloader().find_most_recent_version() + def engine_update_task(engine_class): + logger.debug(f"Checking for updates to {engine_class.name()}") + latest_version = engine_class.downloader().find_most_recent_version() if latest_version: - logger.debug(f"Latest version of {engine.name()} available: {latest_version.get('version')}") - if not cls.is_version_downloaded(engine.name(), latest_version.get('version')): - logger.info(f"Downloading latest version of {engine.name()}...") - cls.download_engine(engine=engine.name(), version=latest_version['version'], background=True) + logger.debug(f"Latest version of {engine_class.name()} available: {latest_version.get('version')}") + if not cls.is_version_downloaded(engine_class.name(), latest_version.get('version')): + logger.info(f"Downloading latest version of {engine_class.name()}...") + cls.download_engine(engine=engine_class.name(), version=latest_version['version'], background=True) else: logger.warning(f"Unable to get check for updates for {engine.name()}") @@ -199,7 +202,6 @@ class EngineManager: threads.append(thread) thread.start() - @classmethod def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None): diff --git a/src/engines/ffmpeg/ffmpeg_downloader.py b/src/engines/ffmpeg/ffmpeg_downloader.py index 638a374..8694ad5 100644 --- a/src/engines/ffmpeg/ffmpeg_downloader.py +++ b/src/engines/ffmpeg/ffmpeg_downloader.py @@ -182,4 +182,5 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') # print(FFMPEGDownloader.download_engine('6.0', '/Users/brett/zordon-uploads/engines/')) # print(FFMPEGDownloader.find_most_recent_version(system_os='linux')) - print(FFMPEGDownloader.download_engine(version='6.0', download_location='/Users/brett/zordon-uploads/engines/', system_os='linux', cpu='x64')) \ No newline at end of file + print(FFMPEGDownloader.download_engine(version='6.0', download_location='/Users/brett/zordon-uploads/engines/', + system_os='linux', cpu='x64')) diff --git a/src/engines/ffmpeg/ffmpeg_engine.py b/src/engines/ffmpeg/ffmpeg_engine.py index 6cfb8ea..6cf4622 100644 --- a/src/engines/ffmpeg/ffmpeg_engine.py +++ b/src/engines/ffmpeg/ffmpeg_engine.py @@ -5,7 +5,6 @@ from src.engines.core.base_engine import * class FFMPEG(BaseRenderEngine): - binary_names = {'linux': 'ffmpeg', 'windows': 'ffmpeg.exe', 'macos': 'ffmpeg'} @staticmethod @@ -83,7 +82,7 @@ class FFMPEG(BaseRenderEngine): def get_encoders(self): raw_stdout = subprocess.check_output([self.renderer_path(), '-encoders'], stderr=subprocess.DEVNULL, timeout=SUBPROCESS_TIMEOUT).decode('utf-8') - pattern = '(?P[VASFXBD.]{6})\s+(?P\S{2,})\s+(?P.*)' + pattern = r'(?P[VASFXBD.]{6})\s+(?P\S{2,})\s+(?P.*)' encoders = [m.groupdict() for m in re.finditer(pattern, raw_stdout)] return encoders @@ -94,7 +93,7 @@ class FFMPEG(BaseRenderEngine): def get_all_formats(self): try: formats_raw = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL, - timeout=SUBPROCESS_TIMEOUT).decode('utf-8') + timeout=SUBPROCESS_TIMEOUT).decode('utf-8') pattern = '(?P[DE]{1,2})\s+(?P\S{2,})\s+(?P.*)' all_formats = [m.groupdict() for m in re.finditer(pattern, formats_raw)] return all_formats @@ -119,8 +118,8 @@ class FFMPEG(BaseRenderEngine): def get_frame_count(self, path_to_file): raw_stdout = subprocess.check_output([self.renderer_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy', - '-f', 'null', '-'], stderr=subprocess.STDOUT, - timeout=SUBPROCESS_TIMEOUT).decode('utf-8') + '-f', 'null', '-'], stderr=subprocess.STDOUT, + timeout=SUBPROCESS_TIMEOUT).decode('utf-8') match = re.findall(r'frame=\s*(\d+)', raw_stdout) if match: frame_number = int(match[-1]) @@ -155,4 +154,4 @@ class FFMPEG(BaseRenderEngine): if __name__ == "__main__": - print(FFMPEG().get_all_formats()) \ No newline at end of file + print(FFMPEG().supported_extensions()) diff --git a/src/engines/ffmpeg/ffmpeg_ui.py b/src/engines/ffmpeg/ffmpeg_ui.py index f28379c..a04a56f 100644 --- a/src/engines/ffmpeg/ffmpeg_ui.py +++ b/src/engines/ffmpeg/ffmpeg_ui.py @@ -2,4 +2,4 @@ class FFMPEGUI: @staticmethod def get_options(instance): options = [] - return options \ No newline at end of file + return options diff --git a/src/engines/ffmpeg/ffmpeg_worker.py b/src/engines/ffmpeg/ffmpeg_worker.py index 49fc80c..2d6ed47 100644 --- a/src/engines/ffmpeg/ffmpeg_worker.py +++ b/src/engines/ffmpeg/ffmpeg_worker.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import re -import subprocess from src.engines.core.base_worker import BaseRenderWorker from src.engines.ffmpeg.ffmpeg_engine import FFMPEG diff --git a/src/ui/add_job.py b/src/ui/add_job.py index bbae15b..cf8c566 100644 --- a/src/ui/add_job.py +++ b/src/ui/add_job.py @@ -21,6 +21,12 @@ from src.utilities.zeroconf_server import ZeroconfServer class NewRenderJobForm(QWidget): def __init__(self, project_path=None): super().__init__() + self.notes_group = None + self.frame_rate_input = None + self.resolution_x_input = None + self.renderer_group = None + self.output_settings_group = None + self.resolution_y_input = None self.project_path = project_path # UI @@ -358,7 +364,7 @@ class NewRenderJobForm(QWidget): text_box = QLineEdit() h_layout.addWidget(text_box) self.renderer_options_layout.addLayout(h_layout) - except AttributeError as e: + except AttributeError: pass def toggle_renderer_enablement(self, enabled=False): diff --git a/src/ui/main_window.py b/src/ui/main_window.py index a8f4ff6..db3c510 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -48,6 +48,11 @@ class MainWindow(QMainWindow): super().__init__() # Load the queue + self.job_list_view = None + self.server_info_ram = None + self.server_info_cpu = None + self.server_info_os = None + self.server_info_hostname = None self.engine_browser_window = None self.server_info_group = None self.current_hostname = None @@ -300,7 +305,7 @@ class MainWindow(QMainWindow): except ConnectionError as e: logger.error(f"Connection error fetching image: {e}") except Exception as e: - logger.error(f"Error fetching image: {e}") + logger.exception(f"Error fetching image: {e}") job_id = self.selected_job_ids()[0] if self.selected_job_ids() else None local_server = is_localhost(self.current_hostname) @@ -339,12 +344,15 @@ class MainWindow(QMainWindow): self.topbar.actions_call['Open Files'].setVisible(False) def selected_job_ids(self): - selected_rows = self.job_list_view.selectionModel().selectedRows() - job_ids = [] - for selected_row in selected_rows: - id_item = self.job_list_view.item(selected_row.row(), 0) - job_ids.append(id_item.text()) - return job_ids + try: + selected_rows = self.job_list_view.selectionModel().selectedRows() + job_ids = [] + for selected_row in selected_rows: + id_item = self.job_list_view.item(selected_row.row(), 0) + job_ids.append(id_item.text()) + return job_ids + except AttributeError: + return [] def refresh_job_headers(self): self.job_list_view.setHorizontalHeaderLabels(["ID", "Name", "Renderer", "Priority", "Status", diff --git a/src/utilities/config.py b/src/utilities/config.py index 97d84cf..1fb435a 100644 --- a/src/utilities/config.py +++ b/src/utilities/config.py @@ -38,7 +38,7 @@ class Config: @classmethod def config_dir(cls): - # Setup the config path + # Set up the config path if current_system_os() == 'macos': local_config_path = os.path.expanduser('~/Library/Application Support/Zordon') elif current_system_os() == 'windows': @@ -49,7 +49,7 @@ class Config: @classmethod def setup_config_dir(cls): - # Setup the config path + # Set up the config path local_config_dir = cls.config_dir() if os.path.exists(local_config_dir): return @@ -71,4 +71,4 @@ class Config: except Exception as e: print(f"An error occurred while setting up the config directory: {e}") - raise \ No newline at end of file + raise diff --git a/src/utilities/ffmpeg_helper.py b/src/utilities/ffmpeg_helper.py index 82cedf8..cb55f19 100644 --- a/src/utilities/ffmpeg_helper.py +++ b/src/utilities/ffmpeg_helper.py @@ -4,9 +4,10 @@ from src.engines.ffmpeg.ffmpeg_engine import FFMPEG def image_sequence_to_video(source_glob_pattern, output_path, framerate=24, encoder="prores_ks", profile=4, start_frame=1): - subprocess.run([FFMPEG.default_renderer_path(), "-framerate", str(framerate), "-start_number", str(start_frame), "-i", - f"{source_glob_pattern}", "-c:v", encoder, "-profile:v", str(profile), '-pix_fmt', 'yuva444p10le', - output_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + subprocess.run([FFMPEG.default_renderer_path(), "-framerate", str(framerate), "-start_number", + str(start_frame), "-i", f"{source_glob_pattern}", "-c:v", encoder, "-profile:v", str(profile), + '-pix_fmt', 'yuva444p10le', output_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=True) def save_first_frame(source_path, dest_path, max_width=1280): diff --git a/src/utilities/misc_helper.py b/src/utilities/misc_helper.py index 13199f5..777c261 100644 --- a/src/utilities/misc_helper.py +++ b/src/utilities/misc_helper.py @@ -36,9 +36,9 @@ def file_exists_in_mounts(filepath): path = os.path.normpath(path) components = [] while True: - path, component = os.path.split(path) - if component: - components.append(component) + path, comp = os.path.split(path) + if comp: + components.append(comp) else: if path: components.append(path) @@ -64,20 +64,17 @@ def file_exists_in_mounts(filepath): def get_time_elapsed(start_time=None, end_time=None): - from string import Template - - class DeltaTemplate(Template): - delimiter = "%" - def strfdelta(tdelta, fmt='%H:%M:%S'): - d = {"D": tdelta.days} + days = tdelta.days hours, rem = divmod(tdelta.seconds, 3600) minutes, seconds = divmod(rem, 60) - d["H"] = '{:02d}'.format(hours) - d["M"] = '{:02d}'.format(minutes) - d["S"] = '{:02d}'.format(seconds) - t = DeltaTemplate(fmt) - return t.substitute(**d) + + # Using f-strings for formatting + formatted_str = fmt.replace('%D', f'{days}') + formatted_str = formatted_str.replace('%H', f'{hours:02d}') + formatted_str = formatted_str.replace('%M', f'{minutes:02d}') + formatted_str = formatted_str.replace('%S', f'{seconds:02d}') + return formatted_str # calculate elapsed time elapsed_time = None @@ -95,7 +92,7 @@ def get_time_elapsed(start_time=None, end_time=None): def get_file_size_human(file_path): size_in_bytes = os.path.getsize(file_path) - # Convert size to a human readable format + # Convert size to a human-readable format if size_in_bytes < 1024: return f"{size_in_bytes} B" elif size_in_bytes < 1024 ** 2: diff --git a/src/utilities/zeroconf_server.py b/src/utilities/zeroconf_server.py index 3a95d5a..50c3340 100644 --- a/src/utilities/zeroconf_server.py +++ b/src/utilities/zeroconf_server.py @@ -22,6 +22,10 @@ class ZeroconfServer: cls.service_type = service_type cls.server_name = server_name cls.server_port = server_port + try: # Stop any previously running instances + socket.gethostbyname(socket.gethostname()) + except socket.gaierror: + cls.stop() @classmethod def start(cls, listen_only=False): @@ -82,7 +86,7 @@ class ZeroconfServer: def found_hostnames(cls): fetched_hostnames = [x.split(f'.{cls.service_type}')[0] for x in cls.client_cache.keys()] local_hostname = socket.gethostname() - # Define a sort key function + def sort_key(hostname): # Return 0 if it's the local hostname so it comes first, else return 1 return False if hostname == local_hostname else True @@ -98,6 +102,7 @@ class ZeroconfServer: decoded_server_info = {key.decode('utf-8'): value.decode('utf-8') for key, value in server_info.items()} return decoded_server_info + # Example usage: if __name__ == "__main__": ZeroconfServer.configure("_zordon._tcp.local.", "foobar.local", 8080)