mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Downloadable engines (#34)
* Add blender_downloader.py * Add engine_manager.py * Add additional methods to engine_manager.py * Refactor file layout to make engines on par with workers * Add system platform info to status response * Default to using system platform / cpu if none are provided * Add API to download an engine and some general cleanup * Add method to delete downloaded engine * Add API calls to download engines and delete downloads * Misc fixes
This commit is contained in:
0
src/engines/__init__.py
Normal file
0
src/engines/__init__.py
Normal file
25
src/engines/aerender_engine.py
Normal file
25
src/engines/aerender_engine.py
Normal file
@@ -0,0 +1,25 @@
|
||||
try:
|
||||
from .base_engine import *
|
||||
except ImportError:
|
||||
from base_engine import *
|
||||
|
||||
|
||||
class AERender(BaseRenderEngine):
|
||||
|
||||
supported_extensions = ['.aep']
|
||||
|
||||
def version(self):
|
||||
version = None
|
||||
try:
|
||||
render_path = self.renderer_path()
|
||||
if render_path:
|
||||
ver_out = subprocess.check_output([render_path, '-version'], timeout=SUBPROCESS_TIMEOUT)
|
||||
version = ver_out.decode('utf-8').split(" ")[-1].strip()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get {self.name()} version: {e}')
|
||||
return version
|
||||
|
||||
@classmethod
|
||||
def get_output_formats(cls):
|
||||
# todo: create implementation
|
||||
return []
|
||||
50
src/engines/base_engine.py
Normal file
50
src/engines/base_engine.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger()
|
||||
SUBPROCESS_TIMEOUT = 5
|
||||
|
||||
|
||||
class BaseRenderEngine(object):
|
||||
|
||||
install_paths = []
|
||||
supported_extensions = []
|
||||
|
||||
def __init__(self, custom_path=None):
|
||||
self.custom_renderer_path = custom_path
|
||||
|
||||
def renderer_path(self):
|
||||
return self.custom_renderer_path or self.default_renderer_path()
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
@classmethod
|
||||
def default_renderer_path(cls):
|
||||
path = None
|
||||
try:
|
||||
path = subprocess.check_output(['which', cls.name()], timeout=SUBPROCESS_TIMEOUT).decode('utf-8').strip()
|
||||
except subprocess.CalledProcessError:
|
||||
for p in cls.install_paths:
|
||||
if os.path.exists(p):
|
||||
path = p
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return path
|
||||
|
||||
def version(self):
|
||||
raise NotImplementedError("version not implemented")
|
||||
|
||||
def get_help(self):
|
||||
path = self.renderer_path()
|
||||
if not path:
|
||||
raise FileNotFoundError("renderer path not found")
|
||||
help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
|
||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||
return help_doc
|
||||
|
||||
@classmethod
|
||||
def get_output_formats(cls):
|
||||
raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")
|
||||
98
src/engines/blender_engine.py
Normal file
98
src/engines/blender_engine.py
Normal file
@@ -0,0 +1,98 @@
|
||||
try:
|
||||
from .base_engine import *
|
||||
except ImportError:
|
||||
from base_engine import *
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
class Blender(BaseRenderEngine):
|
||||
|
||||
install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender']
|
||||
supported_extensions = ['.blend']
|
||||
|
||||
def version(self):
|
||||
version = None
|
||||
try:
|
||||
render_path = self.renderer_path()
|
||||
if render_path:
|
||||
ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT)
|
||||
version = ver_out.decode('utf-8').splitlines()[0].replace('Blender', '').strip()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get Blender version: {e}')
|
||||
return version
|
||||
|
||||
def get_output_formats(self):
|
||||
format_string = self.get_help().split('Format Options')[-1].split('Animation Playback Options')[0]
|
||||
formats = re.findall(r"'([A-Z_0-9]+)'", format_string)
|
||||
return formats
|
||||
|
||||
def run_python_expression(self, project_path, python_expression, timeout=None):
|
||||
if os.path.exists(project_path):
|
||||
try:
|
||||
return subprocess.run([self.renderer_path(), '-b', project_path, '--python-expr', python_expression],
|
||||
capture_output=True, timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running python expression in blender: {e}")
|
||||
pass
|
||||
else:
|
||||
raise FileNotFoundError(f'Project file not found: {project_path}')
|
||||
|
||||
def run_python_script(self, project_path, script_path, timeout=None):
|
||||
if os.path.exists(project_path) and os.path.exists(script_path):
|
||||
try:
|
||||
return subprocess.run([self.default_renderer_path(), '-b', project_path, '--python', script_path],
|
||||
capture_output=True, timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running python expression in blender: {e}")
|
||||
pass
|
||||
elif not os.path.exists(project_path):
|
||||
raise FileNotFoundError(f'Project file not found: {project_path}')
|
||||
elif not os.path.exists(script_path):
|
||||
raise FileNotFoundError(f'Python script not found: {script_path}')
|
||||
raise Exception("Uncaught exception")
|
||||
|
||||
def get_scene_info(self, project_path, timeout=10):
|
||||
scene_info = {}
|
||||
try:
|
||||
results = self.run_python_script(project_path, os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'scripts', 'blender', 'get_file_info.py'), timeout=timeout)
|
||||
result_text = results.stdout.decode()
|
||||
for line in result_text.splitlines():
|
||||
if line.startswith('SCENE_DATA:'):
|
||||
raw_data = line.split('SCENE_DATA:')[-1]
|
||||
scene_info = json.loads(raw_data)
|
||||
break
|
||||
elif line.startswith('Error'):
|
||||
logger.error(f"get_scene_info error: {line.strip()}")
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting file details for .blend file: {e}')
|
||||
return scene_info
|
||||
|
||||
def pack_project_file(self, project_path, timeout=30):
|
||||
# Credit to L0Lock for pack script - https://blender.stackexchange.com/a/243935
|
||||
try:
|
||||
logger.info(f"Starting to pack Blender file: {project_path}")
|
||||
results = self.run_python_script(project_path, os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'scripts', 'blender', 'pack_project.py'), timeout=timeout)
|
||||
|
||||
result_text = results.stdout.decode()
|
||||
dir_name = os.path.dirname(project_path)
|
||||
|
||||
# report any missing textures
|
||||
not_found = re.findall("(Unable to pack file, source path .*)\n", result_text)
|
||||
for err in not_found:
|
||||
logger.error(err)
|
||||
|
||||
p = re.compile('Saved to: (.*)\n')
|
||||
match = p.search(result_text)
|
||||
if match:
|
||||
new_path = os.path.join(dir_name, match.group(1).strip())
|
||||
logger.info(f'Blender file packed successfully to {new_path}')
|
||||
return new_path
|
||||
except Exception as e:
|
||||
logger.error(f'Error packing .blend file: {e}')
|
||||
return None
|
||||
128
src/engines/engine_manager.py
Normal file
128
src/engines/engine_manager.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from .blender_engine import Blender
|
||||
except ImportError:
|
||||
from blender_engine import Blender
|
||||
try:
|
||||
from .ffmpeg_engine import FFMPEG
|
||||
except ImportError:
|
||||
from ffmpeg_engine import FFMPEG
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
class EngineManager:
|
||||
|
||||
engines_path = "~/zordon-uploads/engines"
|
||||
|
||||
@classmethod
|
||||
def supported_engines(cls):
|
||||
return [Blender, FFMPEG]
|
||||
|
||||
@classmethod
|
||||
def all_engines(cls):
|
||||
results = []
|
||||
# Parse downloaded engine directory
|
||||
try:
|
||||
all_items = os.listdir(cls.engines_path)
|
||||
all_directories = [item for item in all_items if os.path.isdir(os.path.join(cls.engines_path, item))]
|
||||
|
||||
for directory in all_directories:
|
||||
# Split the input string by dashes to get segments
|
||||
segments = directory.split('-')
|
||||
|
||||
# Define the keys for each word
|
||||
keys = ["engine", "version", "system_os", "cpu"]
|
||||
|
||||
# Create a dictionary with named keys
|
||||
executable_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender.app'}
|
||||
result_dict = {keys[i]: segments[i] for i in range(min(len(keys), len(segments)))}
|
||||
result_dict['path'] = os.path.join(cls.engines_path, directory, executable_names[result_dict['system_os']])
|
||||
result_dict['type'] = 'managed'
|
||||
results.append(result_dict)
|
||||
except FileNotFoundError:
|
||||
logger.warning("Cannot find local engines download directory")
|
||||
|
||||
# add system installs to this list
|
||||
for eng in cls.supported_engines():
|
||||
if eng.default_renderer_path():
|
||||
results.append({'engine': eng.name(), 'version': eng().version(),
|
||||
'system_os': cls.system_os(),
|
||||
'cpu': cls.system_cpu(),
|
||||
'path': eng.default_renderer_path(), 'type': 'system'})
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def all_versions_for_engine(cls, engine):
|
||||
return [x for x in cls.all_engines() if x['engine'] == engine]
|
||||
|
||||
@classmethod
|
||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
||||
system_os = system_os or cls.system_os()
|
||||
cpu = cpu or cls.system_cpu()
|
||||
|
||||
try:
|
||||
filtered = [x for x in cls.all_engines() if x['engine'] == engine and x['system_os'] == system_os and x['cpu'] == cpu]
|
||||
versions = sorted(filtered, key=lambda x: x['version'], reverse=True)
|
||||
return versions[0]
|
||||
except IndexError:
|
||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def has_engine_version(cls, engine, version, system_os=None, cpu=None):
|
||||
system_os = system_os or cls.system_os()
|
||||
cpu = cpu or cls.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]
|
||||
return filtered[0] if filtered else False
|
||||
|
||||
@staticmethod
|
||||
def system_os():
|
||||
return platform.system().lower().replace('darwin', 'macos')
|
||||
|
||||
@staticmethod
|
||||
def system_cpu():
|
||||
return platform.machine().lower().replace('amd64', 'x64')
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, engine, version, system_os=None, cpu=None):
|
||||
existing_download = cls.has_engine_version(engine, version, system_os, cpu)
|
||||
if existing_download:
|
||||
logger.info(f"Requested download of {engine} {version}, but local copy already exists")
|
||||
return existing_download
|
||||
|
||||
if engine == "blender":
|
||||
from .scripts.blender.blender_downloader import BlenderDownloader
|
||||
logger.info(f"Requesting download of {engine} {version}")
|
||||
if BlenderDownloader.download_engine(version, download_location=cls.engines_path, system_os=system_os, cpu=cpu):
|
||||
return cls.has_engine_version(engine, version, system_os, cpu)
|
||||
else:
|
||||
logger.error("Error downloading Engine")
|
||||
|
||||
return None # Return None to indicate an error
|
||||
|
||||
@classmethod
|
||||
def delete_engine_download(cls, engine, version, system_os=None, cpu=None):
|
||||
logger.info(f"Requested deletion of engine: {engine}-{version}")
|
||||
found = cls.has_engine_version(engine, version, system_os, cpu)
|
||||
if found:
|
||||
dir_path = os.path.dirname(found['path'])
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
logger.info(f"Engine {engine}-{version}-{found['system_os']}-{found['cpu']} successfully deleted")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Cannot find engine: {engine}-{version}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
# print(EngineManager.newest_engine_version('blender', 'macos', 'arm64'))
|
||||
EngineManager.delete_engine_download('blender', '3.2.1', 'windows', 'x64')
|
||||
50
src/engines/ffmpeg_engine.py
Normal file
50
src/engines/ffmpeg_engine.py
Normal file
@@ -0,0 +1,50 @@
|
||||
try:
|
||||
from .base_engine import *
|
||||
except ImportError:
|
||||
from base_engine import *
|
||||
import re
|
||||
|
||||
|
||||
class FFMPEG(BaseRenderEngine):
|
||||
|
||||
def version(self):
|
||||
version = None
|
||||
try:
|
||||
ver_out = subprocess.check_output([self.renderer_path(), '-version'],
|
||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||
match = re.match(".*version\s*(\S+)\s*Copyright", ver_out)
|
||||
if match:
|
||||
version = match.groups()[0]
|
||||
except Exception as e:
|
||||
logger.error("Failed to get FFMPEG version: {}".format(e))
|
||||
return version
|
||||
|
||||
def get_encoders(self):
|
||||
raw_stdout = subprocess.check_output([self.renderer_path(), '-encoders'], stderr=subprocess.DEVNULL,
|
||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||
pattern = '(?P<type>[VASFXBD.]{6})\s+(?P<name>\S{2,})\s+(?P<description>.*)'
|
||||
encoders = [m.groupdict() for m in re.finditer(pattern, raw_stdout)]
|
||||
return encoders
|
||||
|
||||
def get_all_formats(self):
|
||||
raw_stdout = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL,
|
||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||
pattern = '(?P<type>[DE]{1,2})\s+(?P<name>\S{2,})\s+(?P<description>.*)'
|
||||
formats = [m.groupdict() for m in re.finditer(pattern, raw_stdout)]
|
||||
return formats
|
||||
|
||||
def get_output_formats(self):
|
||||
return [x for x in self.get_all_formats() if 'E' in x['type'].upper()]
|
||||
|
||||
def get_frame_count(self, path_to_file):
|
||||
raw_stdout = subprocess.check_output([self.default_renderer_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy',
|
||||
'-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])
|
||||
return frame_number
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(FFMPEG.get_frame_count('/Users/brett/Desktop/Big_Fire_02.mov'))
|
||||
0
src/engines/scripts/__init__.py
Normal file
0
src/engines/scripts/__init__.py
Normal file
0
src/engines/scripts/blender/__init__.py
Normal file
0
src/engines/scripts/blender/__init__.py
Normal file
206
src/engines/scripts/blender/blender_downloader.py
Normal file
206
src/engines/scripts/blender/blender_downloader.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import platform
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
# url = "https://download.blender.org/release/"
|
||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
||||
|
||||
logger = logging.getLogger()
|
||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||
|
||||
|
||||
class BlenderDownloader:
|
||||
|
||||
@staticmethod
|
||||
def get_major_versions():
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Use regex to find all the <a> tags and extract the href attribute
|
||||
link_pattern = r'<a href="([^"]+)">Blender(\d+[^<]+)</a>'
|
||||
link_matches = re.findall(link_pattern, response.text)
|
||||
|
||||
major_versions = [link[-1].strip('/') for link in link_matches]
|
||||
major_versions.sort(reverse=True)
|
||||
return major_versions
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Error: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_minor_versions(major_version, system_os=None, cpu=None):
|
||||
|
||||
base_url = url + 'Blender' + major_version
|
||||
|
||||
response = requests.get(base_url)
|
||||
response.raise_for_status()
|
||||
|
||||
versions_pattern = r'<a href="(?P<file>[^"]+)">blender-(?P<version>[\d\.]+)-(?P<system_os>\w+)-(?P<cpu>\w+).*</a>'
|
||||
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)]
|
||||
|
||||
if system_os:
|
||||
versions_data = [x for x in versions_data if x['system_os'] == system_os]
|
||||
if cpu:
|
||||
versions_data = [x for x in versions_data if x['cpu'] == cpu]
|
||||
|
||||
for v in versions_data:
|
||||
v['url'] = os.path.join(base_url, v['file'])
|
||||
|
||||
versions_data = sorted(versions_data, key=lambda x: x['version'], reverse=True)
|
||||
return versions_data
|
||||
|
||||
@staticmethod
|
||||
def find_LTS_versions():
|
||||
response = requests.get('https://www.blender.org/download/lts/')
|
||||
response.raise_for_status()
|
||||
|
||||
lts_pattern = r'https://www.blender.org/download/lts/(\d+-\d+)/'
|
||||
lts_matches = re.findall(lts_pattern, response.text)
|
||||
lts_versions = [ver.replace('-', '.') for ver in list(set(lts_matches))]
|
||||
lts_versions.sort(reverse=True)
|
||||
|
||||
return lts_versions
|
||||
|
||||
@classmethod
|
||||
def find_most_recent_version(cls, system_os, cpu, lts_only=False):
|
||||
try:
|
||||
major_version = cls.find_LTS_versions()[0] if lts_only else cls.get_major_versions()[0]
|
||||
most_recent = cls.get_minor_versions(major_version, system_os, cpu)[0]
|
||||
return most_recent
|
||||
except IndexError:
|
||||
logger.error("Cannot find a most recent version")
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None):
|
||||
system_os = system_os or platform.system().lower().replace('darwin', 'macos')
|
||||
cpu = cpu or platform.machine().lower().replace('amd64', 'x64')
|
||||
|
||||
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]
|
||||
# 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)
|
||||
except IndexError:
|
||||
logger.error("Cannot find requested engine")
|
||||
|
||||
@classmethod
|
||||
def download_and_extract_app(cls, remote_url, download_location):
|
||||
|
||||
binary_path = None
|
||||
|
||||
# Create a temp download directory
|
||||
temp_download_dir = tempfile.mkdtemp()
|
||||
downloaded_file_path = os.path.join(temp_download_dir, os.path.basename(remote_url))
|
||||
|
||||
try:
|
||||
output_dir_name = os.path.basename(remote_url)
|
||||
for fmt in supported_formats:
|
||||
output_dir_name = output_dir_name.split(fmt)[0]
|
||||
|
||||
if os.path.exists(os.path.join(download_location, output_dir_name)):
|
||||
logger.error(f"Engine download for {output_dir_name} already exists")
|
||||
return
|
||||
|
||||
if not os.path.exists(downloaded_file_path):
|
||||
# Make a GET request to the URL with stream=True to enable streaming
|
||||
logger.info(f"Downloading {output_dir_name} from {remote_url}")
|
||||
response = requests.get(remote_url, stream=True)
|
||||
|
||||
# Check if the request was successful
|
||||
if response.status_code == 200:
|
||||
# Get the total file size from the "Content-Length" header
|
||||
file_size = int(response.headers.get("Content-Length", 0))
|
||||
|
||||
# Create a progress bar using tqdm
|
||||
progress_bar = tqdm(total=file_size, unit="B", unit_scale=True)
|
||||
|
||||
# Open a file for writing in binary mode
|
||||
with open(downloaded_file_path, "wb") as file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
# Write the chunk to the file
|
||||
file.write(chunk)
|
||||
# Update the progress bar
|
||||
progress_bar.update(len(chunk))
|
||||
|
||||
# Close the progress bar
|
||||
progress_bar.close()
|
||||
logger.info(f"Successfully downloaded {os.path.basename(downloaded_file_path)}")
|
||||
else:
|
||||
logger.error(f"Failed to download the file. Status code: {response.status_code}")
|
||||
|
||||
os.makedirs(download_location, exist_ok=True)
|
||||
|
||||
# Extract the downloaded Blender file
|
||||
# Linux - Process .tar.xz files
|
||||
if downloaded_file_path.lower().endswith('.tar.xz'):
|
||||
try:
|
||||
with tarfile.open(downloaded_file_path, 'r:xz') as tar:
|
||||
tar.extractall(path=download_location)
|
||||
os.path.join(download_location, output_dir_name, 'blender')
|
||||
logger.info(f'Successfully extracted {os.path.basename(downloaded_file_path)} to {download_location}')
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f'Error extracting {downloaded_file_path}: {e}')
|
||||
except FileNotFoundError:
|
||||
logger.error(f'File not found: {downloaded_file_path}')
|
||||
|
||||
# Windows - Process .zip files
|
||||
elif downloaded_file_path.lower().endswith('.zip'):
|
||||
try:
|
||||
with zipfile.ZipFile(downloaded_file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(download_location)
|
||||
logger.info(f'Successfully extracted {os.path.basename(downloaded_file_path)} to {download_location}')
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error(f'Error: {downloaded_file_path} is not a valid ZIP file.')
|
||||
except FileNotFoundError:
|
||||
logger.error(f'File not found: {downloaded_file_path}')
|
||||
|
||||
# macOS - Process .dmg files
|
||||
elif downloaded_file_path.lower().endswith('.dmg'):
|
||||
import dmglib
|
||||
dmg = dmglib.DiskImage(downloaded_file_path)
|
||||
for mount_point in dmg.attach():
|
||||
try:
|
||||
# Copy the entire .app bundle to the destination directory
|
||||
shutil.copytree(os.path.join(mount_point, 'Blender.app'),
|
||||
os.path.join(download_location, output_dir_name, 'Blender.app'))
|
||||
binary_path = os.path.join(download_location, output_dir_name, 'Blender.app')
|
||||
logger.info(f'Successfully copied {os.path.basename(downloaded_file_path)} to {download_location}')
|
||||
except FileNotFoundError:
|
||||
logger.error(f'Error: The source .app bundle does not exist.')
|
||||
except PermissionError:
|
||||
logger.error(f'Error: Permission denied to copy {download_location}.')
|
||||
except Exception as e:
|
||||
logger.error(f'An error occurred: {e}')
|
||||
dmg.detach()
|
||||
|
||||
else:
|
||||
logger.error("Unknown file. Unable to extract binary.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# remove downloaded file on completion
|
||||
shutil.rmtree(temp_download_dir)
|
||||
return binary_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
BlenderDownloader.download_engine('3.3.1', download_location="/Users/brett/Desktop/test/releases", system_os='linux', cpu='x64')
|
||||
|
||||
29
src/engines/scripts/blender/get_file_info.py
Normal file
29
src/engines/scripts/blender/get_file_info.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
import bpy
|
||||
|
||||
# Get all cameras
|
||||
cameras = []
|
||||
for cam_obj in bpy.data.cameras:
|
||||
user_map = bpy.data.user_map(subset={cam_obj}, value_types={'OBJECT'})
|
||||
for data_obj in user_map[cam_obj]:
|
||||
cam = {'name': data_obj.name,
|
||||
'cam_name': cam_obj.name,
|
||||
'cam_name_full': cam_obj.name_full,
|
||||
'lens': cam_obj.lens,
|
||||
'lens_unit': cam_obj.lens_unit,
|
||||
'sensor_height': cam_obj.sensor_height,
|
||||
'sensor_width': cam_obj.sensor_width}
|
||||
cameras.append(cam)
|
||||
|
||||
scene = bpy.data.scenes[0]
|
||||
data = {'cameras': cameras,
|
||||
'engine': scene.render.engine,
|
||||
'frame_start': scene.frame_start,
|
||||
'frame_end': scene.frame_end,
|
||||
'resolution_x': scene.render.resolution_x,
|
||||
'resolution_y': scene.render.resolution_y,
|
||||
'resolution_percentage': scene.render.resolution_percentage,
|
||||
'fps': scene.render.fps}
|
||||
|
||||
data_string = json.dumps(data)
|
||||
print("SCENE_DATA:" + data_string)
|
||||
53
src/engines/scripts/blender/pack_project.py
Normal file
53
src/engines/scripts/blender/pack_project.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import bpy
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
|
||||
def zip_files(paths, output_zip_path):
|
||||
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
# If the path is a file, add it to the zip
|
||||
zipf.write(path, arcname=os.path.basename(path))
|
||||
elif os.path.isdir(path):
|
||||
# If the path is a directory, add all its files and subdirectories
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
full_path = os.path.join(root, file)
|
||||
zipf.write(full_path, arcname=os.path.join(os.path.basename(path), os.path.relpath(full_path, path)))
|
||||
|
||||
|
||||
# Get File path
|
||||
project_path = str(bpy.data.filepath)
|
||||
|
||||
# Pack Files
|
||||
bpy.ops.file.pack_all()
|
||||
bpy.ops.file.make_paths_absolute()
|
||||
|
||||
# Temp dir
|
||||
tmp_dir = os.path.join(os.path.dirname(project_path), 'tmp')
|
||||
asset_dir = os.path.join(tmp_dir, 'assets')
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
|
||||
# Find images we could not pack - usually videos
|
||||
for img in bpy.data.images:
|
||||
if not img.packed_file and img.filepath and img.users:
|
||||
os.makedirs(asset_dir, exist_ok=True)
|
||||
shutil.copy2(img.filepath, os.path.join(asset_dir, os.path.basename(img.filepath)))
|
||||
print(f"Copied {os.path.basename(img.filepath)} to tmp directory")
|
||||
img.filepath = '//' + os.path.join('assets', os.path.basename(img.filepath))
|
||||
|
||||
# Save Output
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(tmp_dir, os.path.basename(project_path)), compress=True, relative_remap=False)
|
||||
|
||||
# Save Zip
|
||||
zip_path = os.path.join(os.path.dirname(project_path), f"{os.path.basename(project_path).split('.')[0]}.zip")
|
||||
zip_files([os.path.join(tmp_dir, os.path.basename(project_path)), asset_dir], zip_path)
|
||||
if os.path.exists(zip_path):
|
||||
print(f'Saved to: {zip_path}')
|
||||
else:
|
||||
print("Error saving zip file!")
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
Reference in New Issue
Block a user