Merge branch 'maintenance' into devel
This commit is contained in:
commit
d430fe35c5
11 changed files with 216 additions and 110 deletions
|
|
@ -1,5 +1,13 @@
|
|||
# OctoPrint Changelog
|
||||
|
||||
## 1.3.1rc2 (2017-01-20)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [#1641](https://github.com/foosel/OctoPrint/issues/1641) - Fix issue with `octoprint --daemon` not working. (2nd try)
|
||||
|
||||
([Commits](https://github.com/foosel/OctoPrint/compare/1.3.1rc1...1.3.1rc2))
|
||||
|
||||
## 1.3.1rc1 (2017-01-13)
|
||||
|
||||
### Note for upgraders
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ def set_ctx_obj_option(ctx, param, value):
|
|||
ctx.obj = OctoPrintContext()
|
||||
if value != param.default:
|
||||
setattr(ctx.obj, param.name, value)
|
||||
elif param.default is not None:
|
||||
setattr(ctx.obj, param.name, param.default)
|
||||
|
||||
#~~ helper for retrieving context options
|
||||
|
||||
|
|
@ -139,7 +141,7 @@ from .config import config_commands
|
|||
@legacy_options
|
||||
@click.version_option(version=octoprint.__version__, allow_from_autoenv=False)
|
||||
@click.pass_context
|
||||
def octo(ctx, debug, host, port, logging, daemon, pid, allow_root):
|
||||
def octo(ctx, **kwargs):
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
# We have to support calling the octoprint command without any
|
||||
|
|
@ -148,16 +150,20 @@ def octo(ctx, debug, host, port, logging, daemon, pid, allow_root):
|
|||
# But better print a message to inform people that they should
|
||||
# use the sub commands instead.
|
||||
|
||||
def get_value(key):
|
||||
return get_ctx_obj_option(ctx, key, kwargs.get(key))
|
||||
daemon = get_value("daemon")
|
||||
|
||||
if daemon:
|
||||
click.echo("Daemon operation via \"octoprint --daemon "
|
||||
"start|stop|restart\" is deprecated, please use "
|
||||
"\"octoprint daemon start|stop|restart\" from now on")
|
||||
|
||||
from octoprint.cli.server import daemon_command
|
||||
ctx.invoke(daemon_command, debug=debug, host=host, port=port, logging=logging, allow_root=allow_root, command=daemon, pid=pid)
|
||||
ctx.invoke(daemon_command, command=daemon, **kwargs)
|
||||
else:
|
||||
click.echo("Starting the server via \"octoprint\" is deprecated, "
|
||||
"please use \"octoprint serve\" from now on.")
|
||||
|
||||
from octoprint.cli.server import serve_command
|
||||
ctx.invoke(serve_command, debug=debug, host=host, port=port, logging=logging, allow_root=allow_root)
|
||||
ctx.invoke(serve_command, **kwargs)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
ARCHIVE_EXTENSIONS = (".zip", ".tar.gz", ".tgz", ".tar")
|
||||
|
||||
OPERATING_SYSTEMS = dict(windows=["win32"],
|
||||
linux=["linux2"],
|
||||
macos=["darwin"])
|
||||
|
||||
pip_inapplicable_arguments = dict(uninstall=["--user"])
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -609,10 +613,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
)
|
||||
|
||||
if "compatibility" in entry:
|
||||
if "octoprint" in entry["compatibility"] and entry["compatibility"]["octoprint"] is not None and len(entry["compatibility"]["octoprint"]):
|
||||
if "octoprint" in entry["compatibility"] and entry["compatibility"]["octoprint"] is not None and isinstance(entry["compatibility"]["octoprint"], (list, tuple)) and len(entry["compatibility"]["octoprint"]):
|
||||
result["is_compatible"]["octoprint"] = self._is_octoprint_compatible(octoprint_version, entry["compatibility"]["octoprint"])
|
||||
|
||||
if "os" in entry["compatibility"] and entry["compatibility"]["os"] is not None and len(entry["compatibility"]["os"]):
|
||||
if "os" in entry["compatibility"] and entry["compatibility"]["os"] is not None and isinstance(entry["compatibility"]["os"], (list, tuple)) and len(entry["compatibility"]["os"]):
|
||||
result["is_compatible"]["os"] = self._is_os_compatible(current_os, entry["compatibility"]["os"])
|
||||
|
||||
return result
|
||||
|
|
@ -626,12 +630,15 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
"""
|
||||
|
||||
for octo_compat in compatibility_entries:
|
||||
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
|
||||
octo_compat = ">={}".format(octo_compat)
|
||||
try:
|
||||
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
|
||||
octo_compat = ">={}".format(octo_compat)
|
||||
|
||||
s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat))
|
||||
if octoprint_version in s:
|
||||
break
|
||||
s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat))
|
||||
if octoprint_version in s:
|
||||
break
|
||||
except:
|
||||
self._logger.exception("Something is wrong with this compatibility string for OctoPrint: {}".format(octo_compat))
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -641,15 +648,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
"""
|
||||
Tests if the ``current_os`` matches any of the provided ``compatibility_entries``.
|
||||
"""
|
||||
return current_os in compatibility_entries
|
||||
return current_os in filter(lambda x: x in self.__class__.OPERATING_SYSTEMS.keys(), compatibility_entries)
|
||||
|
||||
def _get_os(self):
|
||||
if sys.platform == "win32":
|
||||
return "windows"
|
||||
elif sys.platform == "linux2":
|
||||
return "linux"
|
||||
elif sys.platform == "darwin":
|
||||
return "macos"
|
||||
for identifier, platforms in self.__class__.OPERATING_SYSTEMS.items():
|
||||
if sys.platform in platforms:
|
||||
return identifier
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
|
|
|
|||
|
|
@ -83,11 +83,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
except:
|
||||
self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals()))
|
||||
else:
|
||||
for key, data in hook_checks.items():
|
||||
for key, default_config in hook_checks.items():
|
||||
check_providers[key] = name
|
||||
|
||||
yaml_config = dict()
|
||||
effective_config = default_config
|
||||
if key in self._configured_checks:
|
||||
data = dict_merge(data, self._configured_checks[key])
|
||||
self._configured_checks[key] = data
|
||||
yaml_config = self._configured_checks[key]
|
||||
effective_config = dict_merge(default_config, yaml_config)
|
||||
|
||||
# Make sure there's nothing persisted in that check that shouldn't be persisted
|
||||
#
|
||||
# This used to be part of the settings migration (version 2) due to a bug - it can't
|
||||
# stay there though since it interferes with manual entries to the checks not
|
||||
# originating from within a plugin. Hence we do that step now here.
|
||||
if "type" not in effective_config or effective_config["type"] != "github_commit":
|
||||
deletables = ["current", "displayVersion"]
|
||||
else:
|
||||
deletables = []
|
||||
self._clean_settings_check(key, yaml_config, default_config, delete=deletables, save=False)
|
||||
|
||||
# finally set our internal representation to our processed result
|
||||
self._configured_checks[key] = effective_config
|
||||
|
||||
self._settings.set(["check_providers"], check_providers)
|
||||
self._settings.save()
|
||||
|
||||
|
|
@ -287,11 +305,18 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._refresh_configured_checks = True
|
||||
|
||||
def get_settings_version(self):
|
||||
return 4
|
||||
return 5
|
||||
|
||||
def on_settings_migrate(self, target, current=None):
|
||||
|
||||
if current is None or current < 4:
|
||||
if current == 4:
|
||||
# config version 4 didn't correctly remove the old settings for octoprint_restart_command
|
||||
# and environment_restart_command
|
||||
|
||||
self._settings.set(["environment_restart_command"], None)
|
||||
self._settings.set(["octoprint_restart_command"], None)
|
||||
|
||||
if current is None or current < 5:
|
||||
# config version 4 and higher moves octoprint_restart_command and
|
||||
# environment_restart_command to the core configuration
|
||||
|
||||
|
|
@ -315,50 +340,27 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
|
||||
if current is None or current == 2:
|
||||
# No config version and config version 2 need the same fix, stripping
|
||||
# accidentally persisted data off the checks
|
||||
# accidentally persisted data off the checks.
|
||||
#
|
||||
# We used to do the same processing for the plugin entries too here, but that interfered
|
||||
# with manual configuration entries. Stuff got deleted that wasn't supposed to be deleted.
|
||||
#
|
||||
# The problem is that we don't know if an entry we are looking at and which didn't come through
|
||||
# a plugin hook is simply an entry from a now uninstalled/unactive plugin, or if it was something
|
||||
# manually configured by the user. So instead of just blindly removing anything that doesn't
|
||||
# come from a plugin here we instead clean up anything that indeed comes from a plugin
|
||||
# during run time and leave everything else as is in the hopes that will not cause trouble.
|
||||
#
|
||||
# We still handle the "octoprint" entry here though.
|
||||
|
||||
configured_checks = self._settings.get(["checks"], incl_defaults=False)
|
||||
if configured_checks is None:
|
||||
configured_checks = dict()
|
||||
|
||||
check_keys = configured_checks.keys()
|
||||
|
||||
# take care of the octoprint entry
|
||||
if "octoprint" in configured_checks:
|
||||
if configured_checks is not None and "octoprint" in configured_checks:
|
||||
octoprint_check = dict(configured_checks["octoprint"])
|
||||
if "type" not in octoprint_check or octoprint_check["type"] != "github_commit":
|
||||
deletables=["current", "displayName", "displayVersion"]
|
||||
else:
|
||||
deletables=[]
|
||||
octoprint_check = self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False)
|
||||
check_keys.remove("octoprint")
|
||||
|
||||
# and the hooks
|
||||
update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config")
|
||||
for name, hook in update_check_hooks.items():
|
||||
try:
|
||||
hook_checks = hook()
|
||||
except:
|
||||
self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals()))
|
||||
else:
|
||||
for key, data in hook_checks.items():
|
||||
if key in configured_checks:
|
||||
settings_check = dict(configured_checks[key])
|
||||
merged = dict_merge(data, settings_check)
|
||||
if "type" not in merged or merged["type"] != "github_commit":
|
||||
deletables = ["current", "displayVersion"]
|
||||
else:
|
||||
deletables = []
|
||||
|
||||
self._clean_settings_check(key, settings_check, data, delete=deletables, save=False)
|
||||
check_keys.remove(key)
|
||||
|
||||
# and anything that's left over we'll just remove now
|
||||
for key in check_keys:
|
||||
dummy_defaults = dict(plugins=dict())
|
||||
dummy_defaults["plugins"][self._identifier] = dict(checks=dict())
|
||||
dummy_defaults["plugins"][self._identifier]["checks"][key] = None
|
||||
self._settings.set(["checks", key], None, defaults=dummy_defaults)
|
||||
self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False)
|
||||
|
||||
elif current == 1:
|
||||
# config version 1 had the error that the octoprint check got accidentally
|
||||
|
|
@ -416,6 +418,15 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
def view():
|
||||
try:
|
||||
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
|
||||
|
||||
# we don't want to transfer python_checker or python_updater values through json - replace with True
|
||||
for key, data in information.items():
|
||||
if "check" in data:
|
||||
if "python_checker" in data["check"]:
|
||||
data["check"]["python_checker"] = True
|
||||
if "python_updater" in data["check"]:
|
||||
data["check"]["python_updater"] = True
|
||||
|
||||
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current",
|
||||
information=information))
|
||||
except exceptions.ConfigurationInvalid as e:
|
||||
|
|
@ -677,6 +688,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
target_restart_type = check["restart"]
|
||||
elif "pip" in check:
|
||||
target_restart_type = "octoprint"
|
||||
else:
|
||||
target_restart_type = None
|
||||
|
||||
# if our update requires a restart we have to determine which type
|
||||
if restart_type is None or (restart_type == "octoprint" and target_restart_type == "environment"):
|
||||
|
|
@ -930,7 +943,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
elif "pip" in check:
|
||||
method = "pip"
|
||||
elif "python_updater" in check:
|
||||
method = "python_updated"
|
||||
method = "python_updater"
|
||||
|
||||
if method is None or (valid_methods and not method in valid_methods):
|
||||
raise exceptions.UnknownUpdateType()
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ def can_perform_update(target, check):
|
|||
|
||||
|
||||
def perform_update(target, check, target_version, log_cb=None):
|
||||
return check["python_updater"].perform_update(target, check, target_version, log_cb=None)
|
||||
return check["python_updater"].perform_update(target, check, target_version, log_cb=log_cb)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
import requests
|
||||
import logging
|
||||
|
||||
from ..exceptions import ConfigurationInvalid
|
||||
|
||||
RELEASE_URL = "https://api.github.com/repos/{user}/{repo}/releases"
|
||||
|
||||
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_release")
|
||||
|
|
@ -207,18 +205,20 @@ def _is_current(release_information, compare_type, custom=None, force_base=True)
|
|||
|
||||
Tests:
|
||||
|
||||
>>> _is_current(dict(remote=dict(value=None))
|
||||
>>> _is_current(dict(remote=dict(value=None)), "python")
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.15"), remote=dict(value="1.2.16")))
|
||||
>>> _is_current(dict(local=dict(value="1.2.15"), remote=dict(value="1.2.16")), "python")
|
||||
False
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")))
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")), "python")
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")), force_base=False)
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")), "python", force_base=False)
|
||||
False
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False)
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), "python", force_base=False)
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False, compare_type="python_unequal")
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), "python_unequal", force_base=False)
|
||||
False
|
||||
>>> _is_current(dict(local=dict(value="1.3.0.post1+g1014712"), remote=dict(value="1.3.0")), "python")
|
||||
True
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -241,6 +241,8 @@ def _is_current(release_information, compare_type, custom=None, force_base=True)
|
|||
|
||||
|
||||
def get_latest(target, check, custom_compare=None):
|
||||
from ..exceptions import ConfigurationInvalid
|
||||
|
||||
if not "user" in check or not "repo" in check:
|
||||
raise ConfigurationInvalid("github_release update configuration for %s needs user and repo set" % target)
|
||||
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ class Server(object):
|
|||
|
||||
return dict(settings=plugin_settings)
|
||||
|
||||
def settings_plugin_config_migration_and_cleanup(name, implementation):
|
||||
def settings_plugin_config_migration_and_cleanup(identifier, implementation):
|
||||
"""Take care of migrating and cleaning up any old settings"""
|
||||
|
||||
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
|
||||
|
|
@ -276,7 +276,7 @@ class Server(object):
|
|||
stored_version = implementation._settings.get_int([octoprint.plugin.SettingsPlugin.config_version_key])
|
||||
if stored_version is None or stored_version < settings_version:
|
||||
settings_migrator(settings_version, stored_version)
|
||||
implementation._settings.set_int([octoprint.plugin.SettingsPlugin.config_version_key], settings_version)
|
||||
implementation._settings.set_int([octoprint.plugin.SettingsPlugin.config_version_key], settings_version, force=True)
|
||||
|
||||
implementation.on_settings_cleanup()
|
||||
implementation._settings.save()
|
||||
|
|
|
|||
|
|
@ -72,9 +72,19 @@ def executeSystemCommand(source, command):
|
|||
if not "command" in command_spec:
|
||||
return make_response("Command {}:{} does not define a command to execute, can't proceed".format(source, command), 500)
|
||||
|
||||
async = command_spec["async"] if "async" in command_spec else False
|
||||
ignore = command_spec["ignore"] if "ignore" in command_spec else False
|
||||
do_async = command_spec.get("async", False)
|
||||
do_ignore = command_spec.get("ignore", False)
|
||||
logger.info("Performing command for {}:{}: {}".format(source, command, command_spec["command"]))
|
||||
|
||||
try:
|
||||
if "before" in command_spec and callable(command_spec["before"]):
|
||||
command_spec["before"]()
|
||||
except Exception as e:
|
||||
if not do_ignore:
|
||||
error = "Command \"before\" failed: {}".format(str(e))
|
||||
logger.warn(error)
|
||||
return make_response(error, 500)
|
||||
|
||||
try:
|
||||
# we run this with shell=True since we have to trust whatever
|
||||
# our admin configured as command and since we want to allow
|
||||
|
|
@ -83,9 +93,9 @@ def executeSystemCommand(source, command):
|
|||
stdout=sarge.Capture(),
|
||||
stderr=sarge.Capture(),
|
||||
shell=True,
|
||||
async=async)
|
||||
if not async:
|
||||
if not ignore and p.returncode != 0:
|
||||
async=do_async)
|
||||
if not do_async:
|
||||
if not do_ignore and p.returncode != 0:
|
||||
returncode = p.returncode
|
||||
stdout_text = p.stdout.text
|
||||
stderr_text = p.stderr.text
|
||||
|
|
@ -94,7 +104,7 @@ def executeSystemCommand(source, command):
|
|||
logger.warn(error)
|
||||
return make_response(error, 500)
|
||||
except Exception as e:
|
||||
if not ignore:
|
||||
if not do_ignore:
|
||||
error = "Command failed: {}".format(str(e))
|
||||
logger.warn(error)
|
||||
return make_response(error, 500)
|
||||
|
|
@ -126,6 +136,10 @@ def _get_command_spec(source, action):
|
|||
|
||||
|
||||
def _get_core_command_specs():
|
||||
def enable_safe_mode():
|
||||
s().set(["server", "startOnceInSafeMode"], True)
|
||||
s().save()
|
||||
|
||||
commands = collections.OrderedDict(
|
||||
shutdown=dict(
|
||||
command=s().get(["server", "commands", "systemShutdownCommand"]),
|
||||
|
|
@ -138,7 +152,12 @@ def _get_core_command_specs():
|
|||
restart=dict(
|
||||
command=s().get(["server", "commands", "serverRestartCommand"]),
|
||||
name=gettext("Restart OctoPrint"),
|
||||
confirm=gettext("You are about to restart the OctoPrint server."))
|
||||
confirm=gettext("You are about to restart the OctoPrint server.")),
|
||||
restart_safe=dict(
|
||||
command=s().get(["server", "commands", "serverRestartCommand"]),
|
||||
name=gettext("Restart OctoPrint in safe mode"),
|
||||
confirm=gettext("You are about to restart the OctoPrint server in safe mode."),
|
||||
before=enable_safe_mode)
|
||||
)
|
||||
|
||||
available_commands = collections.OrderedDict()
|
||||
|
|
|
|||
|
|
@ -1396,13 +1396,18 @@ class Settings(object):
|
|||
|
||||
try:
|
||||
current = chain.get_by_path(path)
|
||||
except KeyError:
|
||||
current = None
|
||||
|
||||
try:
|
||||
default_value = chain.get_by_path(path, only_defaults=True)
|
||||
in_local = chain.has_path(path, only_local=True)
|
||||
in_defaults = chain.has_path(path, only_defaults=True)
|
||||
except KeyError:
|
||||
if error_on_path:
|
||||
raise NoSuchSettingsPath()
|
||||
return
|
||||
default_value = None
|
||||
|
||||
in_local = chain.has_path(path, only_local=True)
|
||||
in_defaults = chain.has_path(path, only_defaults=True)
|
||||
|
||||
if not force and in_defaults and in_local and default_value == value:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -809,17 +809,10 @@ class TimelapseRenderJob(object):
|
|||
|
||||
@classmethod
|
||||
def _create_ffmpeg_command_string(cls, ffmpeg, fps, bitrate, threads, input, output, hflip=False, vflip=False,
|
||||
rotate=False, watermark=None):
|
||||
rotate=False, watermark=None, pixfmt="yuv420p"):
|
||||
"""
|
||||
Create ffmpeg command string based on input parameters.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> TimelapseRenderJob._create_ffmpeg_command_string("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg")
|
||||
'/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -pix_fmt yuv420p -r 25 -y -b 10000k -f vob "/path/to/output.mpg"'
|
||||
>>> TimelapseRenderJob._create_ffmpeg_command_string("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg", hflip=True)
|
||||
'/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -pix_fmt yuv420p -r 25 -y -b 10000k -f vob -vf \\'[in] hflip [out]\\' "/path/to/output.mpg"'
|
||||
|
||||
Arguments:
|
||||
ffmpeg (str): Path to ffmpeg
|
||||
fps (int): Frames per second for output
|
||||
|
|
@ -831,16 +824,19 @@ class TimelapseRenderJob(object):
|
|||
vflip (bool): Perform vertical flip on input material.
|
||||
rotate (bool): Perform 90° CCW rotation on input material.
|
||||
watermark (str): Path to watermark to apply to lower left corner.
|
||||
pixfmt (str): Pixel format to use for output. Default of yuv420p should usually fit the bill.
|
||||
|
||||
Returns:
|
||||
(str): Prepared command string to render `input` to `output` using ffmpeg.
|
||||
"""
|
||||
|
||||
### See unit tests in test/timelapse/test_timelapse_renderjob.py
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
command = [
|
||||
ffmpeg, '-framerate', str(fps), '-loglevel', 'error', '-i', '"{}"'.format(input), '-vcodec', 'mpeg2video',
|
||||
'-threads', str(threads), '-pix_fmt', 'yuv420p', '-r', "25", '-y', '-b', str(bitrate),
|
||||
'-threads', str(threads), '-r', "25", '-y', '-b', str(bitrate),
|
||||
'-f', 'vob']
|
||||
|
||||
filter_string = cls._create_filter_string(hflip=hflip,
|
||||
|
|
@ -859,38 +855,25 @@ class TimelapseRenderJob(object):
|
|||
return " ".join(command)
|
||||
|
||||
@classmethod
|
||||
def _create_filter_string(cls, hflip=False, vflip=False, rotate=False, watermark=None):
|
||||
def _create_filter_string(cls, hflip=False, vflip=False, rotate=False, watermark=None, pixfmt="yuv420p"):
|
||||
"""
|
||||
Creates an ffmpeg filter string based on input parameters.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> TimelapseRenderJob._create_filter_string()
|
||||
>>> TimelapseRenderJob._create_filter_string(hflip=True)
|
||||
'[in] hflip [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(vflip=True)
|
||||
'[in] vflip [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(rotate=True)
|
||||
'[in] transpose=2 [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(vflip=True, rotate=True)
|
||||
'[in] vflip,transpose=2 [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(vflip=True, hflip=True, rotate=True)
|
||||
'[in] hflip,vflip,transpose=2 [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(watermark="/path/to/watermark.png")
|
||||
'movie=/path/to/watermark.png [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]'
|
||||
>>> TimelapseRenderJob._create_filter_string(hflip=True, watermark="/path/to/watermark.png")
|
||||
'[in] hflip [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]'
|
||||
|
||||
Arguments:
|
||||
hflip (bool): Perform horizontal flip on input material.
|
||||
vflip (bool): Perform vertical flip on input material.
|
||||
rotate (bool): Perform 90° CCW rotation on input material.
|
||||
watermark (str): Path to watermark to apply to lower left corner.
|
||||
pixfmt (str): Pixel format to use, defaults to "yuv420p" which should usually fit the bill
|
||||
|
||||
Returns:
|
||||
(str or None): filter string or None if no filters are required
|
||||
"""
|
||||
filters = []
|
||||
|
||||
### See unit tests in test/timelapse/test_timelapse_renderjob.py
|
||||
|
||||
# apply pixel format
|
||||
filters = ["format={}".format(pixfmt)]
|
||||
|
||||
# flip video if configured
|
||||
if hflip:
|
||||
|
|
|
|||
66
tests/timelapse/test_timelapse_renderjob.py
Normal file
66
tests/timelapse/test_timelapse_renderjob.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import unittest
|
||||
|
||||
from ddt import ddt, data, unpack
|
||||
|
||||
from octoprint.timelapse import TimelapseRenderJob
|
||||
|
||||
@ddt
|
||||
class TimelapseRenderJobTest(unittest.TestCase):
|
||||
|
||||
@data(
|
||||
(("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"),
|
||||
dict(),
|
||||
'/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -r 25 -y -b 10000k -f vob -vf \'[in] format=yuv420p [out]\' "/path/to/output.mpg"'),
|
||||
|
||||
(("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"),
|
||||
dict(hflip=True),
|
||||
'/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -r 25 -y -b 10000k -f vob -vf \'[in] format=yuv420p,hflip [out]\' "/path/to/output.mpg"'),
|
||||
|
||||
(("/path/to/ffmpeg", 25, "20000k", 4, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"),
|
||||
dict(rotate=True, watermark="/path/to/watermark.png"),
|
||||
'/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 4 -r 25 -y -b 20000k -f vob -vf \'[in] format=yuv420p,transpose=2 [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]\' "/path/to/output.mpg"')
|
||||
)
|
||||
@unpack
|
||||
def test_create_ffmpeg_command_string(self, args, kwargs, expected):
|
||||
actual = TimelapseRenderJob._create_ffmpeg_command_string(*args, **kwargs)
|
||||
self.assertEquals(actual, expected)
|
||||
|
||||
@data(
|
||||
(dict(),
|
||||
'[in] format=yuv420p [out]'),
|
||||
|
||||
(dict(pixfmt="test"),
|
||||
'[in] format=test [out]'),
|
||||
|
||||
(dict(hflip=True),
|
||||
'[in] format=yuv420p,hflip [out]'),
|
||||
|
||||
(dict(vflip=True),
|
||||
'[in] format=yuv420p,vflip [out]'),
|
||||
|
||||
(dict(rotate=True),
|
||||
'[in] format=yuv420p,transpose=2 [out]'),
|
||||
|
||||
(dict(vflip=True, rotate=True),
|
||||
'[in] format=yuv420p,vflip,transpose=2 [out]'),
|
||||
|
||||
(dict(vflip=True, hflip=True, rotate=True),
|
||||
'[in] format=yuv420p,hflip,vflip,transpose=2 [out]'),
|
||||
|
||||
(dict(watermark="/path/to/watermark.png"),
|
||||
'[in] format=yuv420p [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]'),
|
||||
|
||||
(dict(hflip=True, watermark="/path/to/watermark.png"),
|
||||
'[in] format=yuv420p,hflip [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]'),
|
||||
|
||||
)
|
||||
@unpack
|
||||
def test_create_filter_string(self, kwargs, expected):
|
||||
actual = TimelapseRenderJob._create_filter_string(**kwargs)
|
||||
self.assertEquals(actual, expected)
|
||||
Loading…
Reference in a new issue