Config data validation and warnings with pydantic.

This commit is contained in:
Ian Walton 2020-08-22 13:05:38 -04:00
parent 03e2e3fd64
commit 1e104a8c86
3 changed files with 125 additions and 156 deletions

View File

@ -527,7 +527,7 @@ If you'd like to run the application without installing it, run `./run.py`.
The project is written entirely in Python 3. There are no closed-source
components in this project. It is fully hackable.
The project is dependent on `python-mpv`, `python-mpv-jsonipc`, and `jellyfin-apiclient-python`. If you are
The project is dependent on `python-mpv`, `python-mpv-jsonipc`, `pydantic`, and `jellyfin-apiclient-python`. If you are
using Windows and would like mpv to be maximize properly, `pywin32` is also needed. The GUI
component uses `pystray` and `tkinter`, but there is a fallback cli mode. The mirroring dependencies
are `Jinja2` and `pywebview`, along with platform-specific dependencies. (See the installation and building
@ -550,7 +550,7 @@ The shaders included in the shader pack are also available under verious open so
If you are on Windows there are additional dependencies. Please see the Windows Build Instructions.
1. Install the dependencies: `sudo pip3 install --upgrade python-mpv jellyfin-apiclient-python pystray Jinja2 pywebview python-mpv-jsonipc Flask Werkzeug pypresence`.
1. Install the dependencies: `sudo pip3 install --upgrade python-mpv jellyfin-apiclient-python pystray Jinja2 pywebview python-mpv-jsonipc Flask Werkzeug pypresence pydantic`.
- If you run `./gen_pkg.sh --install`, it will also fetch these for you.
2. Clone this repository: `git clone https://github.com/iwalton3/jellyfin-mpv-shim`
- You can also download a zip build.
@ -661,7 +661,7 @@ You may also need to edit the batch file for 32 bit builds to point to the right
1. Install Git for Windows. Open Git Bash and run `git clone https://github.com/iwalton3/jellyfin-mpv-shim; cd jellyfin-mpv-shim`.
- You can update the project later with `git pull`.
2. Install [Python3](https://www.python.org/downloads/) with PATH enabled. Install [7zip](https://ninite.com/7zip/).
3. After installing python3, open `cmd` as admin and run `pip install --upgrade pyinstaller python-mpv jellyfin-apiclient-python pywin32 pystray Jinja2 pywebview[cef] python-mpv-jsonipc Flask Werkzeug pypresence`.
3. After installing python3, open `cmd` as admin and run `pip install --upgrade pyinstaller python-mpv jellyfin-apiclient-python pywin32 pystray Jinja2 pywebview[cef] python-mpv-jsonipc Flask Werkzeug pypresence pydantic`.
4. Download [libmpv](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/).
5. Extract the `mpv-1.dll` from the file and move it to the `jellyfin-mpv-shim` folder.
6. Open a regular `cmd` prompt. Navigate to the `jellyfin-mpv-shim` folder.

View File

@ -1,180 +1,160 @@
import logging
import os
import uuid
import pickle as pickle
import socket
import json
import os.path
import sys
from pydantic import BaseModel
from typing import Optional
log = logging.getLogger("conf")
config_path = None
class Settings(object):
_listeners = []
_path = None
_data = {
"player_name": socket.gethostname(),
"audio_output": "hdmi",
"client_uuid": str(uuid.uuid4()),
"media_ended_cmd": None,
"pre_media_cmd": None,
"stop_cmd": None,
"auto_play": True,
"idle_cmd": None,
"idle_cmd_delay": 60,
"direct_paths": False,
"remote_direct_paths": False,
"always_transcode": False,
"transcode_h265": False,
"transcode_hi10p": False,
"remote_kbps": 10000,
"local_kbps": 2147483,
"subtitle_size": 100,
"subtitle_color": "#FFFFFFFF",
"subtitle_position": "bottom",
"fullscreen": True,
"enable_gui": True,
"media_key_seek": False,
"mpv_ext": sys.platform.startswith("darwin"),
"mpv_ext_path": None,
"mpv_ext_ipc": None,
"mpv_ext_start": True,
"mpv_ext_no_ovr": False,
"enable_osc": True,
"use_web_seek": False,
"display_mirroring": False,
"log_decisions": False,
"mpv_log_level": "info",
"enable_desktop": False,
"desktop_fullscreen": False,
"desktop_keep_pos": False,
"desktop_keep_size": True,
"idle_when_paused": False,
"stop_idle": False,
"transcode_to_h265": False,
"kb_stop": "q",
"kb_prev": "<",
"kb_next": ">",
"kb_watched": "w",
"kb_unwatched": "u",
"kb_menu": "c",
"kb_menu_esc": "esc",
"kb_menu_ok": "enter",
"kb_menu_left": "left",
"kb_menu_right": "right",
"kb_menu_up": "up",
"kb_menu_down": "down",
"kb_pause": "space",
"kb_fullscreen": "f",
"kb_debug": "~",
"kb_kill_shader": "k",
"seek_up": 60,
"seek_down": -60,
"seek_right": 5,
"seek_left": -5,
"shader_pack_enable": True,
"shader_pack_custom": False,
"shader_pack_remember": True,
"shader_pack_profile": None,
"svp_enable": False,
"svp_url": "http://127.0.0.1:9901/",
"svp_socket": None,
"sanitize_output": True,
"write_logs": False,
"playback_timeout": 30,
"sync_max_delay_speed": 50,
"sync_max_delay_skip": 300,
"sync_method_thresh": 2000,
"sync_speed_time": 1000,
"sync_speed_attempts": 3,
"sync_attempts": 5,
"sync_revert_seek": True,
"sync_osd_message": True,
"screenshot_menu": True,
"check_updates": True,
"notify_updates": True,
"lang": None,
"desktop_scale": 1.0,
"discord_presence": False,
"ignore_ssl_cert": False,
"menu_mouse": True,
"media_keys": True,
"connect_retry_mins": 0,
"transcode_warning": True,
"lang_filter": "und,eng,jpn,mis,mul,zxx",
"lang_filter_sub": False,
"lang_filter_audio": False,
}
def __getattr__(self, name):
return self._data[name]
def __setattr__(self, name, value):
if name in self._data:
self._data[name] = value
self.save()
for callback in self._listeners:
try:
callback(name, value)
except:
pass
else:
super(Settings, self).__setattr__(name, value)
class Settings(BaseModel):
player_name: str = socket.gethostname()
audio_output: str = "hdmi"
client_uuid: str = str(uuid.uuid4())
media_ended_cmd: Optional[str] = None
pre_media_cmd: Optional[str] = None
stop_cmd: Optional[str] = None
auto_play: bool = True
idle_cmd: Optional[str] = None
idle_cmd_delay: int = 60
direct_paths: bool = False
remote_direct_paths: bool = False
always_transcode: bool = False
transcode_h265: bool = False
transcode_hi10p: bool = False
remote_kbps: int = 10000
local_kbps: int = 2147483
subtitle_size: int = 100
subtitle_color: str = "#FFFFFFFF"
subtitle_position: str = "bottom"
fullscreen: bool = True
enable_gui: bool = True
media_key_seek: bool = False
mpv_ext: bool = sys.platform.startswith("darwin")
mpv_ext_path: Optional[str] = None
mpv_ext_ipc: Optional[str] = None
mpv_ext_start: bool = True
mpv_ext_no_ovr: bool = False
enable_osc: bool = True
use_web_seek: bool = False
display_mirroring: bool = False
log_decisions: bool = False
mpv_log_level: str = "info"
enable_desktop: bool = False
desktop_fullscreen: bool = False
desktop_keep_pos: bool = False
desktop_keep_size: bool = True
idle_when_paused: bool = False
stop_idle: bool = False
transcode_to_h265: bool = False
kb_stop: str = "q"
kb_prev: str = "<"
kb_next: str = ">"
kb_watched: str = "w"
kb_unwatched: str = "u"
kb_menu: str = "c"
kb_menu_esc: str = "esc"
kb_menu_ok: str = "enter"
kb_menu_left: str = "left"
kb_menu_right: str = "right"
kb_menu_up: str = "up"
kb_menu_down: str = "down"
kb_pause: str = "space"
kb_fullscreen: str = "f"
kb_debug: str = "~"
kb_kill_shader: str = "k"
seek_up: int = 60
seek_down: int = -60
seek_right: int = 5
seek_left: int = -5
shader_pack_enable: bool = True
shader_pack_custom: bool = False
shader_pack_remember: bool = True
shader_pack_profile: Optional[str] = None
svp_enable: bool = False
svp_url: str = "http://127.0.0.1:9901/"
svp_socket: Optional[str] = None
sanitize_output: bool = True
write_logs: bool = False
playback_timeout: int = 30
sync_max_delay_speed: int = 50
sync_max_delay_skip: int = 300
sync_method_thresh: int = 2000
sync_speed_time: int = 1000
sync_speed_attempts: int = 3
sync_attempts: int = 5
sync_revert_seek: bool = True
sync_osd_message: bool = True
screenshot_menu: bool = True
check_updates: bool = True
notify_updates: bool = True
lang: Optional[str] = None
desktop_scale: float = 1.0
discord_presence: bool = False
ignore_ssl_cert: bool = False
menu_mouse: bool = True
media_keys: bool = True
connect_retry_mins: int = 0
transcode_warning: bool = True
lang_filter: str = "und,eng,jpn,mis,mul,zxx"
lang_filter_sub: bool = False
lang_filter_audio: bool = False
def __get_file(self, path, mode="r", create=True):
created = False
if not os.path.exists(path):
try:
fh = open(path, mode)
_fh = open(path, mode)
except IOError as e:
if e.errno == 2 and create:
fh = open(path, "w")
json.dump(self._data, fh, indent=4, sort_keys=True)
json.dump(self.dict(), fh, indent=4, sort_keys=True)
fh.close()
created = True
else:
raise e
except Exception as e:
except Exception:
log.error("Error opening settings from path: %s" % path)
return None
# This should work now
return open(path, mode), created
def migrate_config(self, old_path, new_path):
fh, created = self.__get_file(old_path, "rb+", False)
if not created:
try:
data = pickle.load(fh)
self._data.update(data)
except Exception as e:
log.error("Error loading settings from pickle: %s" % e)
fh.close()
return False
os.remove(old_path)
self._path = new_path
fh.close()
self.save()
return True
def load(self, path, create=True):
global config_path # Don't want in model.
fh, created = self.__get_file(path, "r", create)
self._path = path
config_path = path
if not created:
try:
data = json.load(fh)
safe_data = self.parse_obj(data)
# Copy and count items
input_params = 0
for key in safe_data.__fields_set__:
setattr(self, key, getattr(safe_data, key))
input_params += 1
# Print warnings
for key, value in data.items():
if key in self._data:
input_params += 1
self._data[key] = value
if key not in safe_data.__fields_set__:
log.warning("Config item {0} was ignored.".format(key))
elif value != getattr(safe_data, key):
log.warning(
"Config item {0} was was coerced from {1} to {2}.".format(
key, repr(value), repr(getattr(safe_data, key))
)
)
log.info("Loaded settings from json: %s" % path)
if input_params < len(self._data):
if input_params < len(self.__fields__):
log.info("Saving back due to schema change.")
self.save()
except Exception as e:
log.error("Error loading settings from json: %s" % e)
@ -185,10 +165,10 @@ class Settings(object):
return True
def save(self):
fh, created = self.__get_file(self._path, "w", True)
fh, created = self.__get_file(config_path, "w", True)
try:
json.dump(self._data, fh, indent=4, sort_keys=True)
json.dump(self.dict(), fh, indent=4, sort_keys=True)
fh.flush()
fh.close()
except Exception as e:
@ -197,17 +177,5 @@ class Settings(object):
return True
def add_listener(self, callback):
"""
Register a callback to be called anytime a setting value changes.
An example callback function:
def my_callback(key, value):
# Do something with the new setting ``value``...
"""
if callback not in self._listeners:
self._listeners.append(callback)
settings = Settings()

View File

@ -53,6 +53,7 @@ setup(
"jellyfin-apiclient-python>=1.6.1",
"python-mpv-jsonipc>=1.1.9",
"requests",
"pydantic",
],
include_package_data=True,
)