kapsikkum-unmanic – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | #!/usr/bin/env python3 |
2 | # -*- coding: utf-8 -*- |
||
3 | |||
4 | """ |
||
5 | unmanic.config.py |
||
6 | |||
7 | Written by: Josh.5 <jsunnex@gmail.com> |
||
8 | Date: 06 Dec 2018, (7:21 AM) |
||
9 | |||
10 | Copyright: |
||
11 | Copyright (C) Josh Sunnex - All Rights Reserved |
||
12 | |||
13 | Permission is hereby granted, free of charge, to any person obtaining a copy |
||
14 | of this software and associated documentation files (the "Software"), to deal |
||
15 | in the Software without restriction, including without limitation the rights |
||
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
17 | copies of the Software, and to permit persons to whom the Software is |
||
18 | furnished to do so, subject to the following conditions: |
||
19 | |||
20 | The above copyright notice and this permission notice shall be included in all |
||
21 | copies or substantial portions of the Software. |
||
22 | |||
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
||
26 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
||
27 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
||
28 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE |
||
29 | OR OTHER DEALINGS IN THE SOFTWARE. |
||
30 | |||
31 | """ |
||
32 | |||
33 | import os |
||
34 | import json |
||
35 | |||
36 | from unmanic import metadata |
||
37 | from unmanic.libs import unlogger |
||
38 | from unmanic.libs import common |
||
39 | from unmanic.libs.singleton import SingletonType |
||
40 | |||
41 | try: |
||
42 | from json.decoder import JSONDecodeError |
||
43 | except ImportError: |
||
44 | JSONDecodeError = ValueError |
||
45 | |||
46 | |||
47 | class Config(object, metaclass=SingletonType): |
||
48 | app_version = '' |
||
49 | |||
50 | test = '' |
||
51 | |||
52 | def __init__(self, config_path=None, **kwargs): |
||
53 | # Set the default UI Port |
||
54 | self.ui_port = 8888 |
||
55 | |||
56 | # Set default directories |
||
57 | home_directory = common.get_home_dir() |
||
58 | self.config_path = os.path.join(home_directory, '.unmanic', 'config') |
||
59 | self.log_path = os.path.join(home_directory, '.unmanic', 'logs') |
||
60 | self.plugins_path = os.path.join(home_directory, '.unmanic', 'plugins') |
||
61 | self.userdata_path = os.path.join(home_directory, '.unmanic', 'userdata') |
||
62 | |||
63 | # Configure debugging |
||
64 | self.debugging = False |
||
65 | |||
66 | # Configure first run (future feature) |
||
67 | self.first_run = False |
||
68 | |||
69 | # Configure first run (future feature) |
||
70 | self.release_notes_viewed = None |
||
71 | |||
72 | # Library Settings: |
||
73 | self.library_path = common.get_default_library_path() |
||
74 | self.enable_library_scanner = False |
||
75 | self.schedule_full_scan_minutes = 1440 |
||
76 | self.follow_symlinks = True |
||
77 | self.concurrent_file_testers = 2 |
||
78 | self.run_full_scan_on_start = False |
||
79 | self.clear_pending_tasks_on_restart = True |
||
80 | self.auto_manage_completed_tasks = False |
||
81 | self.max_age_of_completed_tasks = 91 |
||
82 | self.always_keep_failed_tasks = True |
||
83 | |||
84 | # Worker settings |
||
85 | self.cache_path = common.get_default_cache_path() |
||
86 | |||
87 | # Link settings |
||
88 | self.installation_name = '' |
||
89 | self.remote_installations = [] |
||
90 | self.distributed_worker_count_target = 0 |
||
91 | |||
92 | # Legacy config |
||
93 | # TODO: Remove this before next major version bump |
||
94 | self.number_of_workers = None |
||
95 | self.worker_event_schedules = None |
||
96 | |||
97 | # Import env variables and override all previous settings. |
||
98 | self.__import_settings_from_env() |
||
99 | |||
100 | # Import Unmanic path settings from command params |
||
101 | if kwargs.get('unmanic_path'): |
||
102 | self.set_config_item('config_path', os.path.join(kwargs.get('unmanic_path'), 'config'), save_settings=False) |
||
103 | self.set_config_item('plugins_path', os.path.join(kwargs.get('unmanic_path'), 'plugins'), save_settings=False) |
||
104 | self.set_config_item('userdata_path', os.path.join(kwargs.get('unmanic_path'), 'userdata'), save_settings=False) |
||
105 | |||
106 | # Finally, re-read config from file and override all previous settings. |
||
107 | self.__import_settings_from_file(config_path) |
||
108 | |||
109 | # Overwrite current settings with given args |
||
110 | if config_path: |
||
111 | self.set_config_item('config_path', config_path, save_settings=False) |
||
112 | |||
113 | # Overwrite all other settings passed from command params |
||
114 | if kwargs.get('port'): |
||
115 | self.set_config_item('ui_port', kwargs.get('port'), save_settings=False) |
||
116 | |||
117 | # Apply settings to the unmanic logger |
||
118 | self.__setup_unmanic_logger() |
||
119 | |||
120 | def _log(self, message, message2='', level="info"): |
||
121 | """ |
||
122 | Generic logging method. Can be implemented on any unmanic class |
||
123 | |||
124 | :param message: |
||
125 | :param message2: |
||
126 | :param level: |
||
127 | :return: |
||
128 | """ |
||
129 | unmanic_logging = unlogger.UnmanicLogger.__call__() |
||
130 | logger = unmanic_logging.get_logger(__class__.__name__) |
||
131 | if logger: |
||
132 | message = common.format_message(message, message2) |
||
133 | getattr(logger, level)(message) |
||
134 | else: |
||
135 | print("Unmanic.{} - ERROR!!! Failed to find logger".format(self.__name__)) |
||
136 | |||
137 | def get_config_as_dict(self): |
||
138 | """ |
||
139 | Return a dictionary of configuration fields and their current values |
||
140 | |||
141 | :return: |
||
142 | """ |
||
143 | return self.__dict__ |
||
144 | |||
145 | def get_config_keys(self): |
||
146 | """ |
||
147 | Return a list of configuration fields |
||
148 | |||
149 | :return: |
||
150 | """ |
||
151 | return self.get_config_as_dict().keys() |
||
152 | |||
153 | def __setup_unmanic_logger(self): |
||
154 | """ |
||
155 | Pass configuration to the global logger |
||
156 | |||
157 | :return: |
||
158 | """ |
||
159 | unmanic_logging = unlogger.UnmanicLogger.__call__() |
||
160 | unmanic_logging.setup_logger(self) |
||
161 | |||
162 | def __import_settings_from_env(self): |
||
163 | """ |
||
164 | Read configuration from environment variables. |
||
165 | This is useful for running in a docker container or for unit testing. |
||
166 | |||
167 | :return: |
||
168 | """ |
||
169 | for setting in self.get_config_keys(): |
||
170 | if setting in os.environ: |
||
171 | self.set_config_item(setting, os.environ.get(setting), save_settings=False) |
||
172 | |||
173 | def __import_settings_from_file(self, config_path=None): |
||
174 | """ |
||
175 | Read configuration from the settings JSON file. |
||
176 | |||
177 | :return: |
||
178 | """ |
||
179 | # If config path was not passed as variable, use the default one |
||
180 | if not config_path: |
||
181 | config_path = self.get_config_path() |
||
182 | # Ensure the config path exists |
||
183 | if not os.path.exists(config_path): |
||
184 | os.makedirs(config_path) |
||
185 | settings_file = os.path.join(config_path, 'settings.json') |
||
186 | if os.path.exists(settings_file): |
||
187 | data = {} |
||
188 | try: |
||
189 | with open(settings_file) as infile: |
||
190 | data = json.load(infile) |
||
191 | except Exception as e: |
||
192 | self._log("Exception in reading saved settings from file:", message2=str(e), level="exception") |
||
193 | # Set data to Config class |
||
194 | self.set_bulk_config_items(data, save_settings=False) |
||
195 | |||
196 | def __write_settings_to_file(self): |
||
197 | """ |
||
198 | Dump current settings to the settings JSON file. |
||
199 | |||
200 | :return: |
||
201 | """ |
||
202 | if not os.path.exists(self.get_config_path()): |
||
203 | os.makedirs(self.get_config_path()) |
||
204 | settings_file = os.path.join(self.get_config_path(), 'settings.json') |
||
205 | data = self.get_config_as_dict() |
||
206 | result = common.json_dump_to_file(data, settings_file) |
||
207 | if not result['success']: |
||
208 | for message in result['errors']: |
||
209 | self._log("Error:", message2=str(message), level="error") |
||
210 | raise Exception("Exception in writing settings to file") |
||
211 | |||
212 | def get_config_item(self, key): |
||
213 | """ |
||
214 | Get setting from either this class or the Settings model |
||
215 | |||
216 | :param key: |
||
217 | :return: |
||
218 | """ |
||
219 | # First attempt to fetch it from this class' get functions |
||
220 | if hasattr(self, "get_{}".format(key)): |
||
221 | getter = getattr(self, "get_{}".format(key)) |
||
222 | if callable(getter): |
||
223 | return getter() |
||
224 | |||
225 | def set_config_item(self, key, value, save_settings=True): |
||
226 | """ |
||
227 | Assigns a value to a given configuration field. |
||
228 | This is applied to both this class. |
||
229 | |||
230 | If 'save_settings' is set to False, then settings are only |
||
231 | assigned and not saved to file. |
||
232 | |||
233 | :param key: |
||
234 | :param value: |
||
235 | :param save_settings: |
||
236 | :return: |
||
237 | """ |
||
238 | # Get lowercase value of key |
||
239 | field_id = key.lower() |
||
240 | # Check if key is a valid setting |
||
241 | if field_id not in self.get_config_keys(): |
||
242 | self._log("Attempting to save unknown key", message2=str(key), level="warning") |
||
243 | # Do not proceed if this is any key other than the database |
||
244 | return |
||
245 | |||
246 | # If in a special config list, execute that command |
||
247 | if hasattr(self, "set_{}".format(key)): |
||
248 | setter = getattr(self, "set_{}".format(key)) |
||
249 | if callable(setter): |
||
250 | setter(value) |
||
251 | else: |
||
252 | # Assign value directly to class attribute |
||
253 | setattr(self, key, value) |
||
254 | |||
255 | # Save settings (if requested) |
||
256 | if save_settings: |
||
257 | try: |
||
258 | self.__write_settings_to_file() |
||
259 | except Exception as e: |
||
260 | self._log("Failed to write settings to file: ", message2=str(self.get_config_as_dict()), level="exception") |
||
261 | |||
262 | def set_bulk_config_items(self, items, save_settings=True): |
||
263 | """ |
||
264 | Write bulk config items to this class. |
||
265 | |||
266 | :param items: |
||
267 | :param save_settings: |
||
268 | :return: |
||
269 | """ |
||
270 | # Set values that match the settings model attributes |
||
271 | config_keys = self.get_config_keys() |
||
272 | for config_key in config_keys: |
||
273 | # Only import the item if it exists (Running a get here would default a missing var to None) |
||
274 | if config_key in items: |
||
275 | self.set_config_item(config_key, items[config_key], save_settings=save_settings) |
||
276 | |||
277 | @staticmethod |
||
278 | def read_version(): |
||
279 | """ |
||
280 | Return the application's version number as a string |
||
281 | |||
282 | :return: |
||
283 | """ |
||
284 | return metadata.read_version_string('long') |
||
285 | |||
286 | def read_system_logs(self, lines=None): |
||
287 | """ |
||
288 | Return an array of system log lines |
||
289 | |||
290 | :param lines: |
||
291 | :return: |
||
292 | """ |
||
293 | log_lines = [] |
||
294 | log_file = os.path.join(self.log_path, 'unmanic.log') |
||
295 | line_count = 0 |
||
296 | for line in reversed(list(open(log_file))): |
||
297 | log_lines.insert(0, line.rstrip()) |
||
298 | line_count += 1 |
||
299 | if line_count == lines: |
||
300 | break |
||
301 | return log_lines |
||
302 | |||
303 | def get_ui_port(self): |
||
304 | """ |
||
305 | Get setting - ui_port |
||
306 | |||
307 | :return: |
||
308 | """ |
||
309 | return self.ui_port |
||
310 | |||
311 | def get_cache_path(self): |
||
312 | """ |
||
313 | Get setting - cache_path |
||
314 | |||
315 | :return: |
||
316 | """ |
||
317 | return self.cache_path |
||
318 | |||
319 | def set_cache_path(self, cache_path): |
||
320 | """ |
||
321 | Get setting - cache_path |
||
322 | |||
323 | :return: |
||
324 | """ |
||
325 | if cache_path == "": |
||
326 | self._log("Cache path cannot be empty. Resetting it to default", level="warning") |
||
327 | cache_path = common.get_default_cache_path() |
||
328 | self.cache_path = cache_path |
||
329 | |||
330 | def get_config_path(self): |
||
331 | """ |
||
332 | Get setting - config_path |
||
333 | |||
334 | :return: |
||
335 | """ |
||
336 | return self.config_path |
||
337 | |||
338 | def get_debugging(self): |
||
339 | """ |
||
340 | Get setting - debugging |
||
341 | |||
342 | :return: |
||
343 | """ |
||
344 | return self.debugging |
||
345 | |||
346 | def set_debugging(self, value): |
||
347 | """ |
||
348 | Set setting - debugging |
||
349 | |||
350 | This requires an update to the logger object |
||
351 | |||
352 | :return: |
||
353 | """ |
||
354 | unmanic_logging = unlogger.UnmanicLogger.__call__() |
||
355 | if value: |
||
356 | unmanic_logging.enable_debugging() |
||
357 | else: |
||
358 | unmanic_logging.disable_debugging() |
||
359 | self.debugging = value |
||
360 | |||
361 | def get_first_run(self): |
||
362 | """ |
||
363 | Get setting - first_run |
||
364 | |||
365 | :return: |
||
366 | """ |
||
367 | return self.first_run |
||
368 | |||
369 | def get_release_notes_viewed(self): |
||
370 | """ |
||
371 | Get setting - release_notes_viewed |
||
372 | |||
373 | :return: |
||
374 | """ |
||
375 | return self.release_notes_viewed |
||
376 | |||
377 | def get_library_path(self): |
||
378 | """ |
||
379 | Get setting - library_path |
||
380 | |||
381 | :return: |
||
382 | """ |
||
383 | return self.library_path |
||
384 | |||
385 | def get_clear_pending_tasks_on_restart(self): |
||
386 | """ |
||
387 | Get setting - clear_pending_tasks_on_restart |
||
388 | |||
389 | :return: |
||
390 | """ |
||
391 | return self.clear_pending_tasks_on_restart |
||
392 | |||
393 | def get_auto_manage_completed_tasks(self): |
||
394 | """ |
||
395 | Get setting - auto_manage_completed_tasks |
||
396 | |||
397 | :return: |
||
398 | """ |
||
399 | return self.auto_manage_completed_tasks |
||
400 | |||
401 | def get_max_age_of_completed_tasks(self): |
||
402 | """ |
||
403 | Get setting - max_age_of_completed_tasks |
||
404 | |||
405 | :return: |
||
406 | """ |
||
407 | return self.max_age_of_completed_tasks |
||
408 | |||
409 | def get_always_keep_failed_tasks(self): |
||
410 | """ |
||
411 | Get setting - always_keep_failed_tasks |
||
412 | |||
413 | :return: |
||
414 | """ |
||
415 | return self.always_keep_failed_tasks |
||
416 | |||
417 | def get_log_path(self): |
||
418 | """ |
||
419 | Get setting - log_path |
||
420 | |||
421 | :return: |
||
422 | """ |
||
423 | return self.log_path |
||
424 | |||
425 | def get_number_of_workers(self): |
||
426 | """ |
||
427 | Get setting - number_of_workers |
||
428 | |||
429 | :return: |
||
430 | """ |
||
431 | return self.number_of_workers |
||
432 | |||
433 | def get_worker_event_schedules(self): |
||
434 | """ |
||
435 | Get setting - worker_event_schedules |
||
436 | |||
437 | :return: |
||
438 | """ |
||
439 | return self.worker_event_schedules |
||
440 | |||
441 | def get_enable_library_scanner(self): |
||
442 | """ |
||
443 | Get setting - enable_library_scanner |
||
444 | |||
445 | :return: |
||
446 | """ |
||
447 | return self.enable_library_scanner |
||
448 | |||
449 | def get_run_full_scan_on_start(self): |
||
450 | """ |
||
451 | Get setting - run_full_scan_on_start |
||
452 | |||
453 | :return: |
||
454 | """ |
||
455 | return self.run_full_scan_on_start |
||
456 | |||
457 | def get_schedule_full_scan_minutes(self): |
||
458 | """ |
||
459 | Get setting - schedule_full_scan_minutes |
||
460 | |||
461 | :return: |
||
462 | """ |
||
463 | return self.schedule_full_scan_minutes |
||
464 | |||
465 | def get_follow_symlinks(self): |
||
466 | """ |
||
467 | Get setting - follow_symlinks |
||
468 | |||
469 | :return: |
||
470 | """ |
||
471 | return self.follow_symlinks |
||
472 | |||
473 | def get_concurrent_file_testers(self): |
||
474 | """ |
||
475 | Get setting - concurrent_file_testers |
||
476 | |||
477 | :return: |
||
478 | """ |
||
479 | return self.concurrent_file_testers |
||
480 | |||
481 | def get_plugins_path(self): |
||
482 | """ |
||
483 | Get setting - config_path |
||
484 | |||
485 | :return: |
||
486 | """ |
||
487 | return self.plugins_path |
||
488 | |||
489 | def get_userdata_path(self): |
||
490 | """ |
||
491 | Get setting - userdata_path |
||
492 | |||
493 | :return: |
||
494 | """ |
||
495 | return self.userdata_path |
||
496 | |||
497 | def get_installation_name(self): |
||
498 | """ |
||
499 | Get setting - installation_name |
||
500 | |||
501 | :return: |
||
502 | """ |
||
503 | return self.installation_name |
||
504 | |||
505 | def get_remote_installations(self): |
||
506 | """ |
||
507 | Get setting - remote_installations |
||
508 | |||
509 | :return: |
||
510 | """ |
||
511 | remote_installations = [] |
||
512 | for ri in self.remote_installations: |
||
513 | ri['distributed_worker_count_target'] = self.distributed_worker_count_target |
||
514 | remote_installations.append(ri) |
||
515 | return remote_installations |
||
516 | |||
517 | def get_distributed_worker_count_target(self): |
||
518 | """ |
||
519 | Get setting - distributed_worker_count_target |
||
520 | |||
521 | :return: |
||
522 | """ |
||
523 | return self.distributed_worker_count_target |