Working on config saving
This commit is contained in:
@@ -1,4 +1,19 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
|
||||
from libremsonic.from_json import from_json
|
||||
|
||||
|
||||
class ServerConfiguration:
|
||||
name: str
|
||||
server_address: str
|
||||
local_network_address: str
|
||||
local_network_ssid: str
|
||||
username: str
|
||||
password: str
|
||||
browse_by_tags: bool
|
||||
sync_enabled: bool
|
||||
|
||||
def __init__(self,
|
||||
name='Default',
|
||||
server_address='http://yourhost',
|
||||
@@ -17,3 +32,35 @@ class ServerConfiguration:
|
||||
self.password = password
|
||||
self.browse_by_tags = browse_by_tags
|
||||
self.sync_enabled = sync_enabled
|
||||
|
||||
|
||||
class AppConfiguration:
|
||||
servers: List[ServerConfiguration]
|
||||
current_server: Optional[int]
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
'servers': [s.__dict__ for s in self.servers],
|
||||
'current_server': self.current_server,
|
||||
}
|
||||
|
||||
|
||||
def get_config(filename: str) -> AppConfiguration:
|
||||
with open(filename, 'r') as f:
|
||||
try:
|
||||
response_json = json.load(f)
|
||||
except json.decoder.JSONDecodeError:
|
||||
response_json = None
|
||||
|
||||
if not response_json:
|
||||
default_configuration = AppConfiguration()
|
||||
default_configuration.servers = []
|
||||
default_configuration.current_server = None
|
||||
return default_configuration
|
||||
|
||||
return from_json(AppConfiguration, response_json)
|
||||
|
||||
|
||||
def save_config(config: AppConfiguration, filename: str):
|
||||
with open(filename, 'w+') as f:
|
||||
f.write(json.dumps(config.to_json(), indent=2, sort_keys=True))
|
||||
|
71
libremsonic/from_json.py
Normal file
71
libremsonic/from_json.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from datetime import datetime
|
||||
import typing
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
|
||||
def from_json(cls, data):
|
||||
"""
|
||||
Converts data from a JSON parse into Python data structures.
|
||||
|
||||
Arguments:
|
||||
|
||||
cls: the template class to deserialize into
|
||||
data: the data to deserialize to the class
|
||||
"""
|
||||
# Approach for deserialization here:
|
||||
# https://stackoverflow.com/a/40639688/2319844
|
||||
|
||||
# If it's a forward reference, evaluate it to figure out the actual
|
||||
# type. This allows for types that have to be put into a string.
|
||||
if isinstance(cls, typing.ForwardRef):
|
||||
cls = cls._evaluate(globals(), locals())
|
||||
|
||||
annotations: Dict[str, Type] = getattr(cls, '__annotations__', {})
|
||||
|
||||
# Handle primitive of objects
|
||||
if data is None:
|
||||
instance = None
|
||||
elif cls == str:
|
||||
instance = data
|
||||
elif cls == int:
|
||||
instance = int(data)
|
||||
elif cls == bool:
|
||||
instance = bool(data)
|
||||
elif cls == datetime:
|
||||
instance = parser.parse(data)
|
||||
|
||||
# Handle generics. List[*], Dict[*, *] in particular.
|
||||
elif type(cls) == typing._GenericAlias:
|
||||
# Having to use this because things changed in Python 3.7.
|
||||
class_name = cls._name
|
||||
|
||||
# TODO: this is not very elegant since it doesn't allow things which
|
||||
# sublass from List or Dict.
|
||||
if class_name == 'List':
|
||||
list_type = cls.__args__[0]
|
||||
instance: List[list_type] = list()
|
||||
for value in data:
|
||||
instance.append(from_json(list_type, value))
|
||||
|
||||
elif class_name == 'Dict':
|
||||
key_type, val_type = cls.__args__
|
||||
instance: Dict[key_type, val_type] = dict()
|
||||
for key, value in data.items():
|
||||
key = from_json(key_type, key)
|
||||
value = from_json(val_type, value)
|
||||
instance[key] = value
|
||||
else:
|
||||
raise Exception(
|
||||
'Trying to deserialize an unsupported type: {cls._name}')
|
||||
|
||||
# Handle everything else by first instantiating the class, then adding
|
||||
# all of the sub-elements, recursively calling from_json on them.
|
||||
else:
|
||||
instance: cls = cls()
|
||||
for field, field_type in annotations.items():
|
||||
value = data.get(field)
|
||||
setattr(instance, field, from_json(field_type, value))
|
||||
|
||||
return instance
|
@@ -2,3 +2,5 @@
|
||||
This module defines a stateless server which interops with the Subsonic API.
|
||||
"""
|
||||
from .server import Server
|
||||
|
||||
__all__ = ('Server', )
|
||||
|
@@ -1,73 +1,7 @@
|
||||
import typing
|
||||
from typing import Dict, List, Any, Type
|
||||
from datetime import datetime
|
||||
from dateutil import parser
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def _from_json(cls, data):
|
||||
"""
|
||||
Converts data from a JSON parse into Python data structures.
|
||||
|
||||
Arguments:
|
||||
|
||||
cls: the template class to deserialize into
|
||||
data: the data to deserialize to the class
|
||||
"""
|
||||
# Approach for deserialization here:
|
||||
# https://stackoverflow.com/a/40639688/2319844
|
||||
|
||||
# If it's a forward reference, evaluate it to figure out the actual
|
||||
# type. This allows for types that have to be put into a string.
|
||||
if isinstance(cls, typing.ForwardRef):
|
||||
cls = cls._evaluate(globals(), locals())
|
||||
|
||||
annotations: Dict[str, Type] = getattr(cls, '__annotations__', {})
|
||||
|
||||
# Handle primitive of objects
|
||||
if data is None:
|
||||
instance = None
|
||||
elif cls == str:
|
||||
instance = data
|
||||
elif cls == int:
|
||||
instance = int(data)
|
||||
elif cls == bool:
|
||||
instance = bool(data)
|
||||
elif cls == datetime:
|
||||
instance = parser.parse(data)
|
||||
|
||||
# Handle generics. List[*], Dict[*, *] in particular.
|
||||
elif type(cls) == typing._GenericAlias:
|
||||
# Having to use this because things changed in Python 3.7.
|
||||
class_name = cls._name
|
||||
|
||||
# TODO: this is not very elegant since it doesn't allow things which
|
||||
# sublass from List or Dict.
|
||||
if class_name == 'List':
|
||||
list_type = cls.__args__[0]
|
||||
instance: List[list_type] = list()
|
||||
for value in data:
|
||||
instance.append(_from_json(list_type, value))
|
||||
|
||||
elif class_name == 'Dict':
|
||||
key_type, val_type = cls.__args__
|
||||
instance: Dict[key_type, val_type] = dict()
|
||||
for key, value in data.items():
|
||||
key = _from_json(key_type, key)
|
||||
value = _from_json(val_type, value)
|
||||
instance[key] = value
|
||||
else:
|
||||
raise Exception(
|
||||
'Trying to deserialize an unsupported type: {cls._name}')
|
||||
|
||||
# Handle everything else by first instantiating the class, then adding
|
||||
# all of the sub-elements, recursively calling from_json on them.
|
||||
else:
|
||||
instance: cls = cls()
|
||||
for field, field_type in annotations.items():
|
||||
value = data.get(field)
|
||||
setattr(instance, field, _from_json(field_type, value))
|
||||
|
||||
return instance
|
||||
from libremsonic.from_json import from_json as _from_json
|
||||
|
||||
|
||||
class APIObject:
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import os
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, Gtk
|
||||
|
||||
from libremsonic.config import get_config, save_config
|
||||
|
||||
from .main import MainWindow
|
||||
from .configure_servers import ConfigureServersDialog
|
||||
|
||||
@@ -16,7 +20,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
self.window = None
|
||||
|
||||
# TODO load this from the config file
|
||||
self.current_server = None
|
||||
self.config = None
|
||||
|
||||
def do_startup(self):
|
||||
Gtk.Application.do_startup(self)
|
||||
@@ -35,13 +39,29 @@ class LibremsonicApp(Gtk.Application):
|
||||
self.window.show_all()
|
||||
self.window.present()
|
||||
|
||||
if not self.current_server:
|
||||
self.load_settings()
|
||||
|
||||
if self.config.current_server is None:
|
||||
self.show_configure_servers_dialog()
|
||||
|
||||
print('current config', self.config)
|
||||
|
||||
def on_configure_servers(self, action, param):
|
||||
self.show_configure_servers_dialog()
|
||||
|
||||
def on_server_list_changed(self, action, params):
|
||||
server_config, *_ = params
|
||||
|
||||
self.save_settings()
|
||||
|
||||
def show_configure_servers_dialog(self):
|
||||
dialog = ConfigureServersDialog(self.window)
|
||||
dialog = ConfigureServersDialog(self.window, self.config.servers)
|
||||
dialog.connect('server-list-changed', self.on_server_list_changed)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def load_settings(self):
|
||||
self.config = get_config(os.path.expanduser('~/tmp/test.json'))
|
||||
|
||||
def save_settings(self):
|
||||
save_config(self.config, os.path.expanduser('~/tmp/test.json'))
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import gi
|
||||
import subprocess
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
from libremsonic.server import Server
|
||||
from libremsonic.config import ServerConfiguration
|
||||
|
||||
|
||||
@@ -31,26 +33,29 @@ class EditServerDialog(Gtk.Dialog):
|
||||
|
||||
# Create all of the text entry fields for the server configuration.
|
||||
text_fields = [
|
||||
('Name', existing_config.name),
|
||||
('Server address', existing_config.server_address),
|
||||
('Local network address', existing_config.local_network_address),
|
||||
('Local network SSID', existing_config.local_network_ssid),
|
||||
('Username', existing_config.username),
|
||||
('Password', existing_config.password),
|
||||
('Name', existing_config.name, False),
|
||||
('Server address', existing_config.server_address, False),
|
||||
('Local network address', existing_config.local_network_address,
|
||||
False),
|
||||
('Local network SSID', existing_config.local_network_ssid, False),
|
||||
('Username', existing_config.username, False),
|
||||
('Password', existing_config.password, True),
|
||||
]
|
||||
for label, value in text_fields:
|
||||
for label, value, is_password in text_fields:
|
||||
entry_label = Gtk.Label(label + ':')
|
||||
entry_label.set_halign(Gtk.Align.START)
|
||||
label_box.pack_start(entry_label, True, True, 0)
|
||||
|
||||
entry = Gtk.Entry(text=value)
|
||||
if is_password:
|
||||
entry.set_visibility(False)
|
||||
entry_box.pack_start(entry, True, True, 0)
|
||||
self.data[label] = entry
|
||||
|
||||
# Create all of the check box fields for the server configuration.
|
||||
boolean_fields = [
|
||||
('Browse by tags', existing_config.browse_by_tags),
|
||||
('Sync Enabled', existing_config.sync_enabled),
|
||||
('Sync enabled', existing_config.sync_enabled),
|
||||
]
|
||||
for label, value in boolean_fields:
|
||||
entry_label = Gtk.Label(label + ':')
|
||||
@@ -67,28 +72,41 @@ class EditServerDialog(Gtk.Dialog):
|
||||
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
test_server = Gtk.Button('Test Connection to Server')
|
||||
# TODO: connect to ping
|
||||
test_server.connect('clicked', self.on_test_server_clicked)
|
||||
button_box.pack_start(test_server, False, True, 5)
|
||||
|
||||
open_in_browser = Gtk.Button('Open in Browser')
|
||||
# TODO: connect to open in browser
|
||||
open_in_browser.connect('clicked', self.on_open_in_browser_clicked)
|
||||
button_box.pack_start(open_in_browser, False, True, 5)
|
||||
|
||||
content_area.pack_start(button_box, True, True, 10)
|
||||
|
||||
self.show_all()
|
||||
|
||||
def on_test_server_clicked(self, event):
|
||||
server = Server(
|
||||
self.data['Name'].get_text(),
|
||||
self.data['Server address'].get_text(),
|
||||
self.data['Username'].get_text(),
|
||||
self.data['Password'].get_text(),
|
||||
)
|
||||
server.ping()
|
||||
|
||||
def on_open_in_browser_clicked(self, event):
|
||||
subprocess.call(['xdg-open', self.data['Server address'].get_text()])
|
||||
|
||||
|
||||
class ConfigureServersDialog(Gtk.Dialog):
|
||||
def __init__(self, parent):
|
||||
__gsignals__ = {
|
||||
'server-list-changed': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
|
||||
(object, ))
|
||||
}
|
||||
|
||||
def __init__(self, parent, server_configs):
|
||||
Gtk.Dialog.__init__(self, 'Configure Servers', parent, 0, ())
|
||||
|
||||
# TODO: DEBUG DATA
|
||||
self.server_configs = [
|
||||
ServerConfiguration(name='ohea'),
|
||||
ServerConfiguration()
|
||||
]
|
||||
|
||||
self.server_configs = server_configs
|
||||
self.set_default_size(400, 250)
|
||||
|
||||
flowbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
@@ -126,6 +144,8 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
|
||||
content_area = self.get_content_area()
|
||||
content_area.pack_start(flowbox, True, True, 10)
|
||||
|
||||
self.server_list_on_selected_rows_changed(None)
|
||||
self.show_all()
|
||||
|
||||
def refresh_server_list(self):
|
||||
@@ -139,7 +159,6 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
row.add(server_name_label)
|
||||
self.server_list.add(row)
|
||||
|
||||
print(self.server_list, self.server_configs)
|
||||
self.show_all()
|
||||
|
||||
def on_remove_clicked(self, event):
|
||||
@@ -147,6 +166,7 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
if selected:
|
||||
del self.server_configs[selected.get_index()]
|
||||
self.refresh_server_list()
|
||||
self.emit('server-list-changed', self.server_configs)
|
||||
|
||||
def on_edit_clicked(self, event, add):
|
||||
if add:
|
||||
@@ -167,6 +187,8 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
),
|
||||
username=dialog.data['Username'].get_text(),
|
||||
password=dialog.data['Password'].get_text(),
|
||||
browse_by_tags=dialog.data['Browse by tags'].get_active(),
|
||||
sync_enabled=dialog.data['Sync enabled'].get_active(),
|
||||
)
|
||||
|
||||
if add:
|
||||
@@ -174,9 +196,9 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
else:
|
||||
self.server_configs[selected_index] = new_config
|
||||
|
||||
print([x.name for x in self.server_configs])
|
||||
|
||||
self.refresh_server_list()
|
||||
self.emit('server-list-changed', self.server_configs)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def server_list_on_selected_rows_changed(self, event):
|
||||
|
Reference in New Issue
Block a user