mirror of
https://github.com/blw1138/Zordon.git
synced 2026-06-10 05:59:23 -05:00
552c791207
- Created src/application_context.py as DI container with TYPE_CHECKING imports - server.py now instantiates all services in dependency order via ApplicationContext - Fixed infinite recursion bug: 48 instance methods renamed with underscore prefix to avoid shadowing by same-named @classmethod forwarders - ZeroconfServer: instantiate Zeroconf() in __init__, add _sync_class() to configure forwarder, direct _configure/_start calls during wiring - Config, EngineManager, PreviewManager: all forwarders and _sync_class() intact - RenderQueue: load_state and subscribe moved to __init__, threading.Lock retained - DistributedJobManager: subscribe_to_listener moved to __init__
138 lines
6.0 KiB
Python
138 lines
6.0 KiB
Python
import logging
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Dict, Optional
|
|
|
|
from src.utilities.ffmpeg_helper import generate_thumbnail, save_first_frame
|
|
|
|
logger = logging.getLogger()
|
|
supported_video_formats = ['.mp4', '.mov', '.avi', '.mpg', '.mpeg', '.mxf', '.m4v', '.mkv', '.webm']
|
|
supported_image_formats = ['.jpg', '.png', '.exr', '.tif', '.tga', '.bmp', '.webp']
|
|
|
|
|
|
class PreviewManager:
|
|
_default_instance: Optional['PreviewManager'] = None
|
|
|
|
storage_path: Optional[str] = None
|
|
|
|
def __init__(self) -> None:
|
|
self.storage_path = None
|
|
self._running_jobs: Dict = {}
|
|
|
|
@classmethod
|
|
def _sync_class(cls) -> None:
|
|
if cls._default_instance is not None:
|
|
cls.storage_path = cls._default_instance.storage_path
|
|
|
|
def _generate_job_preview_worker(self, job, replace_existing=False, max_width=480):
|
|
|
|
# Determine best source file to use for thumbs
|
|
job_file_list = job.file_list()
|
|
source_files = job_file_list if job_file_list else [job.input_path]
|
|
preview_label = "output" if job_file_list else "input"
|
|
|
|
# filter by type
|
|
found_image_files = [f for f in source_files if os.path.splitext(f)[-1].lower() in supported_image_formats]
|
|
found_video_files = [f for f in source_files if os.path.splitext(f)[-1].lower() in supported_video_formats]
|
|
|
|
# check if we even have any valid files to work from
|
|
if source_files and not found_video_files and not found_image_files:
|
|
logger.warning(f"No valid image or video files found in files from job: {job}")
|
|
return
|
|
|
|
os.makedirs(self.storage_path, exist_ok=True)
|
|
base_path = os.path.join(self.storage_path, f"{job.id}-{preview_label}-{max_width}")
|
|
preview_video_path = base_path + '.mp4'
|
|
preview_image_path = base_path + '.jpg'
|
|
|
|
if replace_existing:
|
|
for x in [preview_image_path, preview_video_path]:
|
|
try:
|
|
os.remove(x)
|
|
except OSError:
|
|
pass
|
|
|
|
# Generate image previews
|
|
if (found_video_files or found_image_files) and not os.path.exists(preview_image_path):
|
|
try:
|
|
path_of_source = found_image_files[-1] if found_image_files else found_video_files[-1]
|
|
logger.debug(f"Generating image preview for {path_of_source}")
|
|
save_first_frame(source_path=path_of_source, dest_path=preview_image_path, max_width=max_width)
|
|
logger.debug(f"Successfully created image preview for {path_of_source}")
|
|
except Exception as e:
|
|
logger.error(f"Error generating image preview for {job}: {e}")
|
|
|
|
# Generate video previews
|
|
if found_video_files and not os.path.exists(preview_video_path):
|
|
try:
|
|
path_of_source = found_video_files[0]
|
|
logger.debug(f"Generating video preview for {path_of_source}")
|
|
generate_thumbnail(source_path=path_of_source, dest_path=preview_video_path, max_width=max_width)
|
|
logger.debug(f"Successfully created video preview for {path_of_source}")
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error generating video preview for {job}: {e}")
|
|
|
|
def _update_previews_for_job(self, job, replace_existing=False, wait_until_completion=False, timeout=None):
|
|
job_thread = self._running_jobs.get(job.id)
|
|
if job_thread and job_thread.is_alive():
|
|
logger.debug(f'Preview generation job already running for {job}')
|
|
else:
|
|
job_thread = threading.Thread(target=self._generate_job_preview_worker, args=(job, replace_existing,))
|
|
job_thread.start()
|
|
self._running_jobs[job.id] = job_thread
|
|
|
|
if wait_until_completion:
|
|
job_thread.join(timeout=timeout)
|
|
|
|
def _get_previews_for_job(self, job):
|
|
|
|
results = {}
|
|
try:
|
|
directory_path = Path(self.storage_path)
|
|
preview_files_for_job = [f for f in directory_path.iterdir() if f.is_file() and f.name.startswith(job.id)]
|
|
|
|
for preview_filename in preview_files_for_job:
|
|
try:
|
|
pixel_width = str(preview_filename).split('-')[-1]
|
|
preview_label = str(os.path.basename(preview_filename)).split('-')[1]
|
|
extension = os.path.splitext(preview_filename)[-1].lower()
|
|
kind = 'video' if extension in supported_video_formats else \
|
|
'image' if extension in supported_image_formats else 'unknown'
|
|
results[preview_label] = results.get(preview_label, [])
|
|
results[preview_label].append({'filename': str(preview_filename), 'width': pixel_width, 'kind': kind})
|
|
except IndexError: # ignore invalid filenames
|
|
pass
|
|
except FileNotFoundError:
|
|
pass
|
|
return results
|
|
|
|
def _delete_previews_for_job(self, job):
|
|
all_previews = self.get_previews_for_job(job)
|
|
flattened_list = [item for sublist in all_previews.values() for item in sublist]
|
|
for preview in flattened_list:
|
|
try:
|
|
logger.debug(f"Removing preview: {preview['filename']}")
|
|
os.remove(preview['filename'])
|
|
except OSError as e:
|
|
logger.error(f"Error removing preview '{preview.get('filename')}': {e}")
|
|
|
|
# --- Forwarders for backward compatibility ---
|
|
|
|
@classmethod
|
|
def update_previews_for_job(cls, job, replace_existing=False, wait_until_completion=False, timeout=None):
|
|
if cls._default_instance is not None:
|
|
cls._default_instance._update_previews_for_job(job, replace_existing, wait_until_completion, timeout)
|
|
|
|
@classmethod
|
|
def get_previews_for_job(cls, job):
|
|
if cls._default_instance is not None:
|
|
return cls._default_instance._get_previews_for_job(job)
|
|
return {}
|
|
|
|
@classmethod
|
|
def delete_previews_for_job(cls, job):
|
|
if cls._default_instance is not None:
|
|
cls._default_instance._delete_previews_for_job(job)
|