class Settings(BaseModel): """ Settings All application settings """ audio_codec = TextField(null=False, default='aac') audio_stream_encoder = TextField(null=False, default='aac') audio_codec_cloning = TextField(null=False, default='aac') audio_stream_encoder_cloning = TextField(null=False, default='aac') audio_stereo_stream_bitrate = TextField(null=False, default='128k') cache_path = TextField(null=False, default='/tmp/unmanic') config_path = TextField(null=False, default=os.path.join(common.get_home_dir(), '.unmanic', 'config')) keep_filename_history = BooleanField(null=False, default=True) debugging = BooleanField(null=False, default=False) enable_audio_encoding = BooleanField(null=False, default=True) enable_audio_stream_transcoding = BooleanField(null=False, default=True) enable_audio_stream_stereo_cloning = BooleanField(null=False, default=True) enable_inotify = BooleanField(null=False, default=False) enable_video_encoding = BooleanField(null=False, default=True) library_path = TextField(null=False, default='/library') log_path = TextField(null=False, default=os.path.join(common.get_home_dir(), '.unmanic', 'logs')) number_of_workers = IntegerField(null=False, default=3) keep_original_container = BooleanField(null=False, default=False) out_container = TextField(null=False, default='matroska') remove_subtitle_streams = BooleanField(null=False, default=False) run_full_scan_on_start = BooleanField(null=False, default=False) schedule_full_scan_minutes = IntegerField(null=False, default=1440) search_extensions = TextField(null=False, default='mkv,avi,mov,ts,rmvb,mp4,') video_codec = TextField(null=False, default='hevc') video_stream_encoder = TextField(null=False, default='libx265') overwrite_additional_ffmpeg_options = BooleanField(null=False, default=False) additional_ffmpeg_options = TextField(null=True, default='') enable_hardware_accelerated_decoding = BooleanField(null=False, default=False)
def apply_default_db_settings(self, config_path=None): """ Apply the default DB settings. :return: """ if not config_path: config_path = os.path.join(common.get_home_dir(), '.unmanic', 'config') app_dir = os.path.dirname(os.path.abspath(__file__)) self.DATABASE = { "TYPE": "SQLITE", "FILE": os.path.join(config_path, 'unmanic.db'), "MIGRATIONS_DIR": os.path.join(app_dir, 'migrations'), }
def __init__(self, plugins_directory=None): # Set plugins directory if not plugins_directory: home_directory = common.get_home_dir() plugins_directory = os.path.join(home_directory, '.unmanic', 'plugins') self.plugins_directory = plugins_directory # List plugin types in order that they are run # Listing them in order helps for the frontend self.plugin_types = [ { 'id': 'frontend.panel', 'has_flow': False, }, { 'id': 'frontend.plugin_api', 'has_flow': False, }, { 'id': 'library_management.file_test', 'has_flow': True, }, { 'id': 'worker.process_item', 'has_flow': True, }, { 'id': 'postprocessor.file_move', 'has_flow': True, }, { 'id': 'postprocessor.task_result', 'has_flow': True, }, ] unmanic_logging = unlogger.UnmanicLogger.__call__() self.logger = unmanic_logging.get_logger(__class__.__name__)
class CONFIG(object, metaclass=SingletonType): app_version = '' # Set the default UI Port UI_PORT = 8888 # Set default config directory CONFIG_PATH = common.get_config_dir() # Set default plugin directory PLUGINS_PATH = os.path.join(common.get_home_dir(), '.unmanic', 'plugins') # Set default db config DATABASE = None def __init__(self, config_path=None, db_connection=None): # Non config items (objects) self.name = "Config" self.settings = None self.db_connection = db_connection # Apply default DB settings #self.apply_default_db_settings(config_path) # Import env variables and override all previous settings. self.import_settings_from_env() # Read settings from database self.import_settings_from_db() # TODO: Retire this. It is not needed any longer # Finally, re-read config from file and override all previous settings. self.import_settings_from_file(config_path) # Overwrite current settings if config_path: self.set_config_item('config_path', config_path, save_settings=False) # Apply settings to the unmanic logger self.setup_unmanic_logger() # Save settings if self.settings and self.db_connection: self.settings.save() # TODO: Remove temporary beta data migration history_logging = history.History(self) history_logging.migrate_old_beta_data() def _log(self, message, message2='', level="info"): """ Generic logging method. Can be implemented on any unmanic class :param message: :param message2: :param level: :return: """ # TODO: Format all classes with this to fetch a logger unmanic_logging = unlogger.UnmanicLogger.__call__() logger = unmanic_logging.get_logger(self.name) if logger: message = common.format_message(message, message2) getattr(logger, level)(message) else: print("Unmanic.{} - ERROR!!! Failed to find logger".format( self.name)) def get_empty_settings_model(self): """ Return a settings Model object :return: """ if self.settings: return self.settings else: # Fetch blank settings Model object return unmodels.Settings() def get_config_as_dict(self): """ Return a dictionary of configuration fields and their current values :return: """ # Create a copy of this class's dict settings = self.get_empty_settings_model() config_dict = settings.get_current_field_values_dict() # Return dictionary of config items return config_dict def get_config_keys(self): """ Return a list of configuration fields :return: """ keys = self.get_config_as_dict().keys() keys = [item.upper() for item in keys] return keys def setup_unmanic_logger(self): """ Pass configuration to the global logger :return: """ unmanic_logging = unlogger.UnmanicLogger.__call__() unmanic_logging.setup_logger(self) def apply_default_db_settings(self, config_path=None): """ Apply the default DB settings. :return: """ if not config_path: config_path = os.path.join(common.get_home_dir(), '.unmanic', 'config') app_dir = os.path.dirname(os.path.abspath(__file__)) self.DATABASE = { "TYPE": "SQLITE", "FILE": os.path.join(config_path, 'unmanic.db'), "MIGRATIONS_DIR": os.path.join(app_dir, 'migrations'), } def import_settings_from_db(self): """ Read configuration from DB. If configuration does not yet exist, create it first. Configuration is applied to this class with uppercase field names for the sake of simplifying reading throughout the rest of the application. :return: """ # Fetch current settings (create it if nothing yet exists) db_settings = unmodels.Settings() # If there is no DB connection just create an empty settings model if self.db_connection: try: # Fetch a single row (get() will raise DoesNotExist exception if no results are found) self.settings = db_settings.select().limit(1).get() except: # Create settings (defaults will be applied) self.settings = db_settings.create() else: self.settings = self.get_empty_settings_model() # Check if key is a valid setting current_settings = self.get_config_as_dict() for setting in current_settings: # Import settings self.set_config_item(setting.upper(), current_settings[setting], save_settings=False) def import_settings_from_env(self): """ Read configuration from environment variables. This is useful for running in a docker container or for unit testing. :return: """ for setting in self.get_config_keys(): if setting in os.environ: self.set_config_item(setting, os.environ.get(setting), save_settings=False) def import_settings_from_file(self, config_path=None): """ Read configuration from the settings JSON file. :return: """ # If config path was not passed as variable, use the default one if not config_path: config_path = self.get_config_path() # Ensure the config path exists if not os.path.exists(config_path): os.makedirs(config_path) settings_file = os.path.join(config_path, 'settings.json') if os.path.exists(settings_file): data = {} try: with open(settings_file) as infile: data = json.load(infile) except Exception as e: self._log("Exception in reading saved settings from file:", message2=str(e), level="exception") # Set values that match the settings model attributes current_config = self.get_config_keys() for item in current_config: if item in data: self.set_config_item(item, data[item], save_settings=False) # Set config values that are in the 'DATABASE' or 'UI_PORT' keys (if provided) if 'DATABASE' in data: setattr(self, 'DATABASE', data['DATABASE']) if 'UI_PORT' in data: setattr(self, 'UI_PORT', data['UI_PORT']) def write_settings_to_file(self): """ Dump current settings to the settings JSON file. :return: """ if not os.path.exists(self.get_config_path()): os.makedirs(self.get_config_path()) settings_file = os.path.join(self.get_config_path(), 'settings.json') data = self.get_config_as_dict() data = {k.upper(): v for k, v in data.items()} result = common.json_dump_to_file(data, settings_file) if not result['success']: for message in result['errors']: self._log("Exception in writing settings to file:", message2=str(message), level="exception") def get_config_item(self, key): """ Get setting from either this class or the Settings model :param key: :return: """ # First attempt to fetch it from this class' get functions if hasattr(self, "get_{}".format(key)): getter = getattr(self, "get_{}".format(key)) if callable(getter): return getter() # Second attempt to fetch it from the Settings Model if hasattr(self.settings, key): return getattr(self.settings, key) def set_config_item(self, key, value, save_settings=True): """ Assigns a value to a given configuration field. This is applied to both this class and the Settings Model. If 'save_settings' is set to False, then settings are only assigned and not saved to either file or database. :param key: :param value: :param save_settings: :return: """ if key == 'DATABASE': # Only save database settings to file # Database settings are not stored in the database O_o self.__dict__[key] = value if save_settings: self.write_settings_to_file() else: # Get lowercase value of key field_id = key.lower() # Check if key is a valid setting if field_id not in self.get_config_as_dict().keys(): self._log("Attempting to save unknown key", message2=str(key), level="warning") # Do not proceed if this is any key other than the database return settings_model = self.get_empty_settings_model() # Parse field value by it's type for this setting (bool, string, etc.) parsed_field_value = settings_model.parse_field_value_by_type( field_id, value) if self.settings: # Assign value to setting field setattr(self.settings, field_id, parsed_field_value) # Assign field type converted value to class variable # TODO: Remove this once we have migrated to using only the settings model object setattr(self, key, parsed_field_value) # Save settings (if requested) if save_settings: self.write_settings_to_file() if self.settings and self.db_connection: self.settings.save() def allowed_search_extensions(self): """ Return a tuple of the configured extensions to search for. :return: """ search_extensions = self.get_search_extensions() if isinstance(search_extensions, str): # Split the comma separated sting into a list value = search_extensions.split(",") # Strip all whitespace (including within the item as extensions dont have any whitespace) value = [item.replace(' ', '') for item in value] # Remove empty values from the list value = [item for item in value if item] return value return list(search_extensions) def file_ends_in_allowed_search_extensions(self, file_name): # Get the file extension file_extension = os.path.splitext(file_name)[-1][1:] # Ensure the file's extension is lowercase file_extension = file_extension.lower() self._log("Check file_extension", file_extension, level="debug") # Get the list of configured extensions to search for allowed_search_extensions = self.allowed_search_extensions() self._log("Check allowed_search_extensions", allowed_search_extensions, level="debug") # Check if it ends with one of the allowed search extensions if file_extension in allowed_search_extensions: return True return False def read_version(self): """ Return the application's version number as a string :return: """ if not self.app_version: self.app_version = metadata.read_version_string('long') return self.app_version def get_supported_containers(self): """ Return a list of containers supported by unmanic :return: """ return unffmpeg.containers.get_all_containers() def get_all_supported_codecs(self): """ Return a list of all codecs supported by unmanic :return: """ return unffmpeg.Info().get_all_supported_codecs() def get_supported_audio_codecs(self): """ Return a list of audio codecs supported by unmanic :return: """ supported_codecs = self.get_all_supported_codecs() if 'audio' not in supported_codecs: return {} return supported_codecs['audio'] def get_supported_video_codecs(self): """ Return a list of video codecs supported by unmanic :return: """ supported_codecs = self.get_all_supported_codecs() if 'video' not in supported_codecs: return {} return supported_codecs['video'] def get_audio_stream_encoder(self): """ Get setting - audio_stream_encoder :return: """ supported_codecs = self.get_all_supported_codecs() if 'audio' not in supported_codecs: return '' return self.AUDIO_STREAM_ENCODER def get_audio_codec_cloning(self): """ Get setting - audio_codec_cloning :return: """ return self.AUDIO_CODEC_CLONING def get_audio_stream_encoder_cloning(self): """ Get setting - audio_stream_encoder_cloning :return: """ return self.AUDIO_STREAM_ENCODER_CLONING def get_audio_stereo_stream_bitrate(self): """ Get setting - audio_stereo_stream_bitrate :return: """ return self.AUDIO_STEREO_STREAM_BITRATE def get_cache_path(self): """ Get setting - cache_path :return: """ return self.CACHE_PATH def get_config_path(self): """ Get setting - config_path :return: """ return self.CONFIG_PATH def get_keep_filename_history(self): """ Get setting - keep_filename_history :return: """ return self.KEEP_FILENAME_HISTORY def get_debugging(self): """ Get setting - debugging :return: """ return self.DEBUGGING def get_enable_audio_encoding(self): """ Get setting - enable_audio_encoding :return: """ return self.ENABLE_AUDIO_ENCODING def get_enable_audio_stream_transcoding(self): """ Get setting - enable_audio_stream_transcoding :return: """ return self.ENABLE_AUDIO_STREAM_TRANSCODING def get_enable_audio_stream_stereo_cloning(self): """ Get setting - enable_audio_stream_stereo_cloning :return: """ return self.ENABLE_AUDIO_STREAM_STEREO_CLONING def get_enable_inotify(self): """ Get setting - enable_inotify :return: """ return self.ENABLE_INOTIFY def get_enable_video_encoding(self): """ Get setting - enable_video_encoding :return: """ return self.ENABLE_VIDEO_ENCODING def get_library_path(self): """ Get setting - library_path :return: """ return self.LIBRARY_PATH def get_log_path(self): """ Get setting - log_path :return: """ return self.LOG_PATH def get_number_of_workers(self): """ Get setting - number_of_workers :return: """ return self.NUMBER_OF_WORKERS def get_out_container(self): """ Get setting - out_container :return: """ return self.OUT_CONTAINER def get_remove_subtitle_streams(self): """ Get setting - remove_subtitle_streams :return: """ return self.REMOVE_SUBTITLE_STREAMS def get_run_full_scan_on_start(self): """ Get setting - run_full_scan_on_start :return: """ return self.RUN_FULL_SCAN_ON_START def get_schedule_full_scan_minutes(self): """ Get setting - schedule_full_scan_minutes :return: """ return self.SCHEDULE_FULL_SCAN_MINUTES def get_search_extensions(self): """ Get setting - search_extensions :return: """ return self.SEARCH_EXTENSIONS def get_video_codec(self): """ Get setting - video_codec :return: """ return self.settings.video_codec def get_video_stream_encoder(self): """ Get setting - video_stream_encoder :return: """ supported_codecs = self.get_all_supported_codecs() if 'video' not in supported_codecs: return '' return self.VIDEO_STREAM_ENCODER def get_overwrite_additional_ffmpeg_options(self): """ Get setting - overwrite_additional_ffmpeg_options :return: """ return self.OVERWRITE_ADDITIONAL_FFMPEG_OPTIONS def get_additional_ffmpeg_options(self): """ Get setting - additional_ffmpeg_options :return: """ return self.ADDITIONAL_FFMPEG_OPTIONS def get_enable_hardware_accelerated_decoding(self): """ Get setting - enable_hardware_accelerated_decoding :return: """ return self.ENABLE_HARDWARE_ACCELERATED_DECODING def get_plugins_path(self): """ Get setting - config_path :return: """ return self.PLUGINS_PATH
def __init__(self, config_path=None, **kwargs): # Set the default UI Port self.ui_port = 8888 # Set default directories self.config_path = os.path.join(common.get_home_dir(), '.unmanic', 'config') self.log_path = os.path.join(common.get_home_dir(), '.unmanic', 'logs') self.plugins_path = os.path.join(common.get_home_dir(), '.unmanic', 'plugins') self.userdata_path = os.path.join(common.get_home_dir(), '.unmanic', 'userdata') # Configure debugging self.debugging = False # Configure first run (future feature) self.first_run = False # Configure first run (future feature) self.release_notes_viewed = None # Library Settings: self.library_path = common.get_default_library_path() self.enable_library_scanner = False self.schedule_full_scan_minutes = 1440 self.follow_symlinks = True self.concurrent_file_testers = 2 self.run_full_scan_on_start = False self.clear_pending_tasks_on_restart = True self.auto_manage_completed_tasks = False self.max_age_of_completed_tasks = 91 self.always_keep_failed_tasks = True # Worker settings self.cache_path = common.get_default_cache_path() # Link settings self.installation_name = '' self.remote_installations = [] self.distributed_worker_count_target = 0 # Legacy config # TODO: Remove this before next major version bump self.number_of_workers = None self.worker_event_schedules = None # Import env variables and override all previous settings. self.__import_settings_from_env() # Import Unmanic path settings from command params if kwargs.get('unmanic_path'): self.set_config_item('config_path', os.path.join(kwargs.get('unmanic_path'), 'config'), save_settings=False) self.set_config_item('plugins_path', os.path.join(kwargs.get('unmanic_path'), 'plugins'), save_settings=False) self.set_config_item('userdata_path', os.path.join(kwargs.get('unmanic_path'), 'userdata'), save_settings=False) # Finally, re-read config from file and override all previous settings. self.__import_settings_from_file(config_path) # Overwrite current settings with given args if config_path: self.set_config_item('config_path', config_path, save_settings=False) # Overwrite all other settings passed from command params if kwargs.get('port'): self.set_config_item('ui_port', kwargs.get('port'), save_settings=False) # Apply settings to the unmanic logger self.__setup_unmanic_logger()