mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Major file reorganization (#26)
* Major file reorganization * Rearrange imports * Fix default log level
This commit is contained in:
0
src/utilities/__init__.py
Normal file
0
src/utilities/__init__.py
Normal file
20
src/utilities/ffmpeg_helper.py
Normal file
20
src/utilities/ffmpeg_helper.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import subprocess
|
||||
from src.workers.engines.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.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], check=True)
|
||||
|
||||
|
||||
def save_first_frame(source_path, dest_path, max_width=1280):
|
||||
subprocess.run([FFMPEG.renderer_path(), '-i', source_path, '-vf', f'scale={max_width}:-1',
|
||||
'-vframes', '1', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||
|
||||
|
||||
def generate_thumbnail(source_path, dest_path, max_width=240, fps=12):
|
||||
subprocess.run([FFMPEG.renderer_path(), '-i', source_path, '-vf',
|
||||
f"scale={max_width}:trunc(ow/a/2)*2,format=yuv420p", '-r', str(fps), '-c:v', 'libx264', '-preset',
|
||||
'ultrafast', '-an', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||
105
src/utilities/misc_helper.py
Normal file
105
src/utilities/misc_helper.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def launch_url(url):
|
||||
if subprocess.run(['which', 'xdg-open'], capture_output=True).returncode == 0:
|
||||
subprocess.run(['xdg-open', url]) # linux
|
||||
elif subprocess.run(['which', 'open'], capture_output=True).returncode == 0:
|
||||
subprocess.run(['open', url]) # macos
|
||||
elif subprocess.run(['which', 'start'], capture_output=True).returncode == 0:
|
||||
subprocess.run(['start', url]) # windows - need to validate this works
|
||||
else:
|
||||
logger.error(f"No valid launchers found to launch url: {url}")
|
||||
|
||||
|
||||
def file_exists_in_mounts(filepath):
|
||||
"""
|
||||
Check if a file exists in any mounted directory.
|
||||
It searches for the file in common mount points like '/Volumes', '/mnt', and '/media'.
|
||||
Returns the path to the file in the mount if found, otherwise returns None.
|
||||
|
||||
Example:
|
||||
Before: filepath = '/path/to/file.txt'
|
||||
After: '/Volumes/ExternalDrive/path/to/file.txt'
|
||||
"""
|
||||
|
||||
def get_path_components(path):
|
||||
path = os.path.normpath(path)
|
||||
components = []
|
||||
while True:
|
||||
path, component = os.path.split(path)
|
||||
if component:
|
||||
components.append(component)
|
||||
else:
|
||||
if path:
|
||||
components.append(path)
|
||||
break
|
||||
components.reverse()
|
||||
return components
|
||||
|
||||
# Get path components of the directory of the file
|
||||
path_components = get_path_components(os.path.dirname(filepath).strip('/'))
|
||||
|
||||
# Iterate over possible root paths - this may need to be rethought for Windows support
|
||||
for root in ['/Volumes', '/mnt', '/media']:
|
||||
if os.path.exists(root):
|
||||
# Iterate over mounts in the root path
|
||||
for mount in os.listdir(root):
|
||||
# Since we don't know the home directory, we iterate until we find matching parts of the path
|
||||
matching_components = [s for s in path_components if s in mount]
|
||||
for component in matching_components:
|
||||
possible_mount_path = os.path.join(root, mount, filepath.split(component)[-1].lstrip('/'))
|
||||
if os.path.exists(possible_mount_path):
|
||||
return possible_mount_path
|
||||
|
||||
|
||||
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}
|
||||
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)
|
||||
|
||||
# calculate elapsed time
|
||||
elapsed_time = None
|
||||
|
||||
if start_time:
|
||||
if end_time:
|
||||
elapsed_time = end_time - start_time
|
||||
else:
|
||||
elapsed_time = datetime.now() - start_time
|
||||
|
||||
elapsed_time_string = strfdelta(elapsed_time) if elapsed_time else None
|
||||
return elapsed_time_string
|
||||
|
||||
|
||||
def get_file_size_human(file_path):
|
||||
size_in_bytes = os.path.getsize(file_path)
|
||||
|
||||
# Convert size to a human readable format
|
||||
if size_in_bytes < 1024:
|
||||
return f"{size_in_bytes} B"
|
||||
elif size_in_bytes < 1024 ** 2:
|
||||
return f"{size_in_bytes / 1024:.2f} KB"
|
||||
elif size_in_bytes < 1024 ** 3:
|
||||
return f"{size_in_bytes / 1024 ** 2:.2f} MB"
|
||||
elif size_in_bytes < 1024 ** 4:
|
||||
return f"{size_in_bytes / 1024 ** 3:.2f} GB"
|
||||
else:
|
||||
return f"{size_in_bytes / 1024 ** 4:.2f} TB"
|
||||
|
||||
47
src/utilities/server_helper.py
Normal file
47
src/utilities/server_helper.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from src.utilities.ffmpeg_helper import generate_thumbnail, save_first_frame
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def generate_thumbnail_for_job(job, thumb_video_path, thumb_image_path, max_width=320):
|
||||
|
||||
# Simple thread to generate thumbs in background
|
||||
def generate_thumb_thread(source):
|
||||
in_progress_path = thumb_video_path + '_IN-PROGRESS'
|
||||
subprocess.run(['touch', in_progress_path])
|
||||
try:
|
||||
logger.debug(f"Generating video thumbnail for {source}")
|
||||
generate_thumbnail(source_path=source, dest_path=thumb_video_path, max_width=max_width)
|
||||
except subprocess.CalledProcessError as err:
|
||||
logger.error(f"Error generating video thumbnail for {source}: {err}")
|
||||
|
||||
try:
|
||||
os.remove(in_progress_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Determine best source file to use for thumbs
|
||||
source_files = job.file_list() or [job.input_path]
|
||||
if source_files:
|
||||
video_formats = ['.mp4', '.mov', '.avi', '.mpg', '.mpeg', '.mxf', '.m4v', 'mkv']
|
||||
image_formats = ['.jpg', '.png', '.exr']
|
||||
|
||||
image_files = [f for f in source_files if os.path.splitext(f)[-1].lower() in image_formats]
|
||||
video_files = [f for f in source_files if os.path.splitext(f)[-1].lower() in video_formats]
|
||||
|
||||
if (video_files or image_files) and not os.path.exists(thumb_image_path):
|
||||
try:
|
||||
path_of_source = image_files[0] if image_files else video_files[0]
|
||||
logger.debug(f"Generating image thumbnail for {path_of_source}")
|
||||
save_first_frame(source_path=path_of_source, dest_path=thumb_image_path, max_width=max_width)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception saving first frame: {e}")
|
||||
|
||||
if video_files and not os.path.exists(thumb_video_path):
|
||||
x = threading.Thread(target=generate_thumb_thread, args=(video_files[0],))
|
||||
x.start()
|
||||
20
src/utilities/status_utils.py
Normal file
20
src/utilities/status_utils.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RenderStatus(Enum):
|
||||
NOT_STARTED = "not_started"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
ERROR = "error"
|
||||
SCHEDULED = "scheduled"
|
||||
WAITING_FOR_SUBJOBS = "waiting_for_subjobs"
|
||||
CONFIGURING = "configuring"
|
||||
UNDEFINED = "undefined"
|
||||
|
||||
|
||||
def string_to_status(string):
|
||||
for stat in RenderStatus:
|
||||
if stat.value == string:
|
||||
return stat
|
||||
return RenderStatus.UNDEFINED
|
||||
86
src/utilities/zeroconf_server.py
Normal file
86
src/utilities/zeroconf_server.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
class ZeroconfServer:
|
||||
service_type = None
|
||||
server_name = None
|
||||
server_port = None
|
||||
server_ip = None
|
||||
zeroconf = Zeroconf()
|
||||
service_info = None
|
||||
client_cache = {}
|
||||
properties = {}
|
||||
|
||||
@classmethod
|
||||
def configure(cls, service_type, server_name, server_port):
|
||||
cls.service_type = service_type
|
||||
cls.server_name = server_name
|
||||
cls.server_port = server_port
|
||||
|
||||
@classmethod
|
||||
def start(cls, listen_only=False):
|
||||
if not listen_only:
|
||||
cls._register_service()
|
||||
cls._browse_services()
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
cls._unregister_service()
|
||||
cls.zeroconf.close()
|
||||
|
||||
@classmethod
|
||||
def _register_service(cls):
|
||||
cls.server_ip = socket.gethostbyname(socket.gethostname())
|
||||
|
||||
info = ServiceInfo(
|
||||
cls.service_type,
|
||||
f"{cls.server_name}.{cls.service_type}",
|
||||
addresses=[socket.inet_aton(cls.server_ip)],
|
||||
port=cls.server_port,
|
||||
properties=cls.properties,
|
||||
)
|
||||
|
||||
cls.service_info = info
|
||||
cls.zeroconf.register_service(info)
|
||||
logger.info(f"Registered zeroconf service: {cls.service_info.name}")
|
||||
|
||||
@classmethod
|
||||
def _unregister_service(cls):
|
||||
if cls.service_info:
|
||||
cls.zeroconf.unregister_service(cls.service_info)
|
||||
logger.info(f"Unregistered zeroconf service: {cls.service_info.name}")
|
||||
cls.service_info = None
|
||||
|
||||
@classmethod
|
||||
def _browse_services(cls):
|
||||
browser = ServiceBrowser(cls.zeroconf, cls.service_type, [cls._on_service_discovered])
|
||||
browser.is_alive()
|
||||
|
||||
@classmethod
|
||||
def _on_service_discovered(cls, zeroconf, service_type, name, state_change):
|
||||
info = zeroconf.get_service_info(service_type, name)
|
||||
logger.debug(f"Zeroconf: {name} {state_change}")
|
||||
if service_type == cls.service_type:
|
||||
if state_change == ServiceStateChange.Added or state_change == ServiceStateChange.Updated:
|
||||
cls.client_cache[name] = info
|
||||
else:
|
||||
cls.client_cache.pop(name)
|
||||
|
||||
@classmethod
|
||||
def found_clients(cls):
|
||||
return [x.split(f'.{cls.service_type}')[0] for x in cls.client_cache.keys()]
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
ZeroconfServer.configure("_zordon._tcp.local.", "foobar.local", 8080)
|
||||
try:
|
||||
ZeroconfServer.start()
|
||||
input("Server running - Press enter to end")
|
||||
finally:
|
||||
ZeroconfServer.stop()
|
||||
Reference in New Issue
Block a user