diff --git a/libremsonic/config.py b/libremsonic/config.py index 2fa508c..e446676 100644 --- a/libremsonic/config.py +++ b/libremsonic/config.py @@ -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)) diff --git a/libremsonic/from_json.py b/libremsonic/from_json.py new file mode 100644 index 0000000..2737ccd --- /dev/null +++ b/libremsonic/from_json.py @@ -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 diff --git a/libremsonic/server/__init__.py b/libremsonic/server/__init__.py index 3502b79..956bb4c 100644 --- a/libremsonic/server/__init__.py +++ b/libremsonic/server/__init__.py @@ -2,3 +2,5 @@ This module defines a stateless server which interops with the Subsonic API. """ from .server import Server + +__all__ = ('Server', ) diff --git a/libremsonic/server/api_objects.py b/libremsonic/server/api_objects.py index 4873019..053e694 100644 --- a/libremsonic/server/api_objects.py +++ b/libremsonic/server/api_objects.py @@ -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: diff --git a/libremsonic/ui/app.py b/libremsonic/ui/app.py index bcb0806..b04d6b1 100644 --- a/libremsonic/ui/app.py +++ b/libremsonic/ui/app.py @@ -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')) diff --git a/libremsonic/ui/configure_servers.py b/libremsonic/ui/configure_servers.py index 8842efd..1b5d53b 100644 --- a/libremsonic/ui/configure_servers.py +++ b/libremsonic/ui/configure_servers.py @@ -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):